Alink教程(Java版)

第28.1节 中文情感分析

情感分析是对带有情感色彩(褒义贬义/正向负向)的主观性文本进行分析,以确定该文本的观点、喜好、情感倾向。本节将针对顾客对酒店的评论数据,进行建模,并通过模型进行预测。演示情感分析中的常用操作,包括分词、文本向量化等,并使用逻辑回归算法进行建模、预测。后面的章节将会介绍如何BERT向量化及BERT文本分类器。

28.1.1 数据探索

使用的酒店评论数据集链接为:https://raw.githubusercontent.com/SophonPlus/ChineseNlpCorpus/master/datasets/ChnSentiCorp_htl_all/ChnSentiCorp_htl_all.csv


每条记录包括评论内容和标记喜好的标签,标签只有2个值:1代表喜欢,0为不喜欢。下图显示了4条数据:

下面我们使用Alink来进行分析、建模。


首先,将数据文件ChnSentiCorp_htl_all.txt下载到本地目录

static final String DATA_DIR = Utils.ROOT_DIR + "sentiment_hotel" + File.separator;

除了原始数据文件,我们还需要将其分为训练集和预测集,便于进行模型评估以及模型间的对比。设置相应文件名称如下:

static final String ORIGIN_FILE = "ChnSentiCorp_htl_all.csv";
static final String TEST_FILE = "test.ak";
static final String TRAIN_FILE = "train.ak";


使用CsvSourceBatchOp读取URL数据,代码如下。设置列名分别为label和review,数据类型分别为整型和字符串类型,由于该CSV数据第一行保存的是列名,需要设置读取数据时忽略第一行。然后,使用Utils中的训练预测集划分方法splitTrainTestIfNotExist,按9:1的比例,将原数据划分为训练和预测集,并分别保存到文件TRAIN_FILE和TEST_FILE。

CsvSourceBatchOp source = new CsvSourceBatchOp()
	.setFilePath(DATA_DIR + ORIGIN_FILE)
	.setSchemaStr("label int, review string")
	.setIgnoreFirstLine(true);

Utils.splitTrainTestIfNotExist(source, DATA_DIR + TRAIN_FILE, DATA_DIR + TEST_FILE, 0.9);

source.firstN(5).print();


最后一行代码是选择5条数据打印显示出来,结果如下:

label|review
-----|------
0|携程订单上写明:“房型特点:一张1.3*2,另一张1.1*2米,无法加床全部房间有免费宽带”,实际入住后见到的房间是我所见过的最小的标准间,两个床尺寸相同,床的两边均靠着墙,没有一点多余的空间,甚至连放行李的地方也没有,更不要提什么橱了,跟酒店提出与所订房间不同,他们说我们订的房间空调坏了,只能换这个房间,价格一样。我不知道这是酒店的欺诈还是携程的欺诈,我要求得到赔偿。如果需要证据的话,我有实拍的房间照片。另外,建议大家不要出泰山旅游,几乎一半的酒店会不定时,无预警的停电。
0|洗澡竟然没有热水!!太郁闷了~~第二天还要爬山啊!!不过网速还可以~
0|我要求里写了要安静的房间。。谁知道那天住在6楼,外面的风,凄惨的吹着,声音很大,一个小时才入睡。让酒店换房间。他们说,酒店周围没有树,每间房间声音都很大。。赫赫,这里理由么?看来下次,起风的时候就不能去住了。
0|过了好久才想起来评价,记得离火车站超级近,不过方便的同时必然会觉得比较吵。韩日旅游团住这里的很多,前台服务冷淡。两个人住标准间,只给一张房卡,还很挑衅的看我。气的没心情。宾馆反馈2008年7月17日:酒店针对客人提出的问题,现已认真整改,希望每一位入住渤海明珠酒店的您都能高兴入住,满意而归。
0|酒店在铁路旁,晚上火



28.1.2 使用LR算法

我们设置Pipeline,将整个处理和模型过程封装在里面,代码如下:

        new Pipeline()
            .add(
                new Imputer()
                    .setSelectedCols("review")
                    .setOutputCols("featureText")
                    .setStrategy("value")
                    .setFillValue("null")
            )
            .add(
                new Segment()
                    .setSelectedCol("featureText")
            )
            .add(
                new StopWordsRemover()
                    .setSelectedCol("featureText")
            )
            .add(
                new DocCountVectorizer()
                    .setFeatureType("TF")
                    .setSelectedCol("featureText")
                    .setOutputCol("featureVector")
            )
            .add(
                new LogisticRegression()
                    .setVectorCol("featureVector")
                    .setLabelCol("label")
                    .setPredictionCol("pred")
                    .setPredictionDetailCol("pred_info")
            )
            .fit(train_set)
            .save(DATA_DIR + "lr_pipeline_model.ak", true);

        BatchOperator.execute();

这里,解释一下各个算法组件的作用:

  1. Imputer:对“review”列进行缺失值填充,方式是填充字符串值“null”,结果写到“featureText“列。
  2. Segment:是进行分词操作,即将原句子分解为单词,之间用空格分隔。由于没有输入结果列,分词结果会直接替换调输入列的值。
  3. StopWordsRemover:是将分词结果中的停用词去掉。
  4. DocCountVectorizer:对“featureText“列出现的单词进行统计,并根据计算出的TF值,将句子映射为向量,向量长度为单词个数,并保存在"featureVector"列。
  5. LogisticRegression:是使用LogisticRegression分类模型。分类预测放在“pred” 列。

随后,我们就可以进入模型训练阶段。通过Pipeline的fit()方法,可以得到整个流程的模型(PipelineModel),并将模型保存到文件“lr_pipeline_model.ak”。

注意:save()函数的第一个参数为模型保存路径,第二个参数为可选参数,表示是否覆盖写PipelineModel文件,默认值为false。


使用PipelineModel进行批式/流式预测,具体代码如下:

        PipelineModel.load(DATA_DIR + "lr_pipeline_model.ak")
            .transform(test_set)
            .lazyPrint(5)
            .link(
                new EvalBinaryClassBatchOp()
                    .setLabelCol("label")
                    .setPredictionDetailCol("pred_info")
                    .lazyPrintMetrics("LR")
            );
        BatchOperator.execute();


导入上面训练的PipelineModel模型后,可以调用transform方法对批式/流式数据进行预测,最后,使用 EvalBinaryClassBatchOp组件进行二分类模型评估。具体代码如下:

        PipelineModel.load(DATA_DIR + "lr_pipeline_model.ak")
            .transform(test_set)
            .lazyPrint(5)
            .link(
                new EvalBinaryClassBatchOp()
                    .setLabelCol("label")
                    .setPredictionDetailCol("pred_info")
                    .lazyPrintMetrics("LR")
            );
        BatchOperator.execute();


运行结果为:

label|review|featureText|featureVector|pred|pred_info
-----|------|-----------|-------------|----|---------
0|1。房间很小,我住的是标准间A(2008年春节房价430元),根本不值,比上海市内的还要贵;2。空调的温度调节器很糟糕,搞懂该怎么调会费你许多时间,不信可以去试试,哈哈哈哈哈哈。。。不过房间还是很温暖,温度很高,建议开点窗户睡觉,以防被热醒;3。房间也不太干净,地毯脏兮兮的;4。送的早餐太简单,只能算充饥;5。下次不会再入住顺利大酒店,打算去住“莫泰崇明八一店”。|1 房间 很小 住 标准间 2008 年 春节 房价 430 元 根本 不值 上海市 内 还要 贵 2 空调 温度 调节器 很 糟糕 搞懂 调 会费 时间 不信 去 试试 哈哈哈 哈哈哈 房间 很 温暖 温度 很 高 建议 开点 窗户 睡觉 以防 热醒 3 房间 不 太 干净 地毯 脏兮兮 4 送 早餐 太 简单 只能 算 充饥 5 下次 不会 再 入住 顺利 大酒店 打算 去 住 莫泰 崇明 八一 店|$27815$326:0.0136986301369863 728:0.0136986301369863 2347:0.0136986301369863 2630:0.0136986301369863 3464:0.0136986301369863 3635:0.0136986301369863 3656:0.0136986301369863 3837:0.0136986301369863 4761:0.0136986301369863 5254:0.0136986301369863 6161:0.0136986301369863 6336:0.0136986301369863 6351:0.0136986301369863 6586:0.0136986301369863 6649:0.0136986301369863 7158:0.0136986301369863 8585:0.0136986301369863 9147:0.0136986301369863 9149:0.0273972602739726 10970:0.0136986301369863 11023:0.0136986301369863 12052:0.0136986301369863 12159:0.0136986301369863 12254:0.0136986301369863 14089:0.0136986301369863 14296:0.0410958904109589 14385:0.0136986301369863 15288:0.0136986301369863 15321:0.0410958904109589 15599:0.0136986301369863 15679:0.0136986301369863 15795:0.0136986301369863 15981:0.0136986301369863 16054:0.0136986301369863 17941:0.0273972602739726 18075:0.0136986301369863 18861:0.0136986301369863 19482:0.0273972602739726 20243:0.0136986301369863 20645:0.0273972602739726 22298:0.0136986301369863 22341:0.0136986301369863 22590:0.0136986301369863 22691:0.0136986301369863 22807:0.0136986301369863 22855:0.0136986301369863 23431:0.0273972602739726 25622:0.0136986301369863 25624:0.0136986301369863 25636:0.0136986301369863 25663:0.0136986301369863 25696:0.0136986301369863 25800:0.0136986301369863 27275:0.0136986301369863 27308:0.0136986301369863 27337:0.0136986301369863 27453:0.0136986301369863 27579:0.0136986301369863 27601:0.0136986301369863 27753:0.0136986301369863|0|{"0":"0.9609607522710057","1":"0.03903924772899425"}
0|55555555,我住过最差的酒店之一,奉劝大家还是跑远一点吧。|55555555 住 最差 酒店 奉劝 大家 跑 远 一点|$27815$1941:0.1111111111111111 2492:0.1111111111111111 3187:0.1111111111111111 11773:0.1111111111111111 17727:0.1111111111111111 18214:0.1111111111111111 23431:0.1111111111111111 26237:0.1111111111111111 27244:0.1111111111111111|0|{"0":"0.9999984620460199","1":"1.537953980057516E-6"}
0|6月16号就从网上订了房间,晚上到酒店时前台说房间了,也没找到我的订单.后来打电话给携程说已经酒店已经将订单回传过.酒店前台一直让我等待了五十分钟才给了房间.到房间后居然还有别人剩下的垃圾没有清理.而且环境还不好,外面就是马路,吵死了,反正这次弄得相当不愉快,相当差劲点|6 月 16 号 网上 订 房间 晚上 酒店 时 前台 说 房间 没 找到 订单 . 后来 打电话 携程 说 已经 酒店 已经 订单 回传 . 酒店 前台 等待 五十分钟 才 房间 . 房间 后 居然 别人 剩下 垃圾 没有 清理 . 环境 还 不好 外面 马路 吵死 反正 这次 弄 相当 不 愉快 相当 差劲 点|$27815$376:0.017241379310344827 1941:0.05172413793103448 2566:0.017241379310344827 2693:0.017241379310344827 3758:0.034482758620689655 4002:0.034482758620689655 4011:0.017241379310344827 5762:0.017241379310344827 6391:0.017241379310344827 7413:0.034482758620689655 8182:0.017241379310344827 8765:0.017241379310344827 9206:0.017241379310344827 9935:0.017241379310344827 10039:0.017241379310344827 11729:0.017241379310344827 11990:0.017241379310344827 12245:0.017241379310344827 13114:0.017241379310344827 13985:0.017241379310344827 14095:0.017241379310344827 14193:0.017241379310344827 14296:0.06896551724137931 14679:0.017241379310344827 15551:0.017241379310344827 16228:0.034482758620689655 16295:0.017241379310344827 16540:0.017241379310344827 18459:0.017241379310344827 18738:0.017241379310344827 19130:0.017241379310344827 19681:0.017241379310344827 19809:0.017241379310344827 19832:0.017241379310344827 20060:0.017241379310344827 20538:0.017241379310344827 21461:0.017241379310344827 21521:0.034482758620689655 21649:0.017241379310344827 25501:0.017241379310344827 25663:0.017241379310344827 27224:0.017241379310344827 27650:0.017241379310344827 27802:0.06896551724137931|0|{"0":"0.9999986175936274","1":"1.3824063725964564E-6"}
0|8月4日入住了该酒店,没想到很多地方不如意,不得不缩短行程,提前返京。之前在网上查询,想住一个特色酒店,就选择此处的豪华标准间。但没想到:(1)酒店周围正在拆迁,非常脏和吵。出租车司机让我们三年以后看,说一定变样。分配给我们的房子临马路,面对一大施工场,极其嘈杂。不得以要求将其中的两间房调换。前台很不情愿给换了,告知换的房子会比这个小。当时为求安静,只好同意,但后来发现上大当了。(2)调换的两间房估计为普通标准间,屋子里墙壁和柜橱有斑驳的潮湿痕迹,其中一间屋子抽水马桶经常坏。房间很小,书桌和床之间仅容一人过。最可恶的是窗户没有纱窗,且朝过道,冒着进蚊子的威胁,开窗后发现无法通气,因过道窗户紧紧封闭,房间里一股霉味和油漆味散发不出去。更有甚者,因是围拢的房屋结构,早晚有人在过道说话很闹,且过道的灯笼整夜亮着。透过菲薄的窗帘,不得不忍受光污染。屋子的隔音效果很差,需要忍受隔壁看电视的声音。没有中央空调。(3)服务水平一般。在调换房间的第二天,讯问前台要求换到豪华标间,前台说因是自己要求调换的,不补差价,且说现房即为折后338元的价位。这可是在承德!前台服务很差,虽身着民族服装很漂亮,但态度冷冰冰。但对我之前的一老外可是鞠躬哈腰的。(4)早餐难以下咽。因周围是村庄,只能在酒店用早餐。四五个小咸菜;两种粥、豆包、包子以及奶粉冲得很稀的牛奶。即便这种早餐过了八点半还就没有了。开始打扫卫生,不管是否客人吃完。我非常非常后悔选择了这个酒店,性价比最差的。本想休息几天,但住宿实在太差,干脆一家人,勉强住了两夜就退房走人。|8 月 4 日 入住 酒店 没想到 很多 地方 不如意 不得不 缩短 行程 提前 返京 之前 网上 查询 想住 特色 酒店 选择 此处 豪华 标准间 没想到 1 酒店 周围 正在 拆迁 非常 脏 吵 出租车 司机 三年 以后 看 说 一定 变样 分配 房子 马路 面对 一大 施工 场 极其 嘈杂 不得 要求 两间房 调换 前台 很 不 情愿 换 告知 换 房子 会 小 当时 为求 安静 只好 同意 后来 发现 上大当 2 调换 两间房 估计 普通 标准间 屋子里 墙壁 柜橱 斑驳 潮湿 痕迹 一间 屋子 抽水马桶 经常 坏 房间 很小 书桌 床 之间 仅容 一人过 最 可恶 窗户 没有 纱窗 朝过 道 进 蚊子 威胁 开窗 后 发现 无法 通气 因过 道 窗户 紧紧 封闭 房间 里 一股 霉味 油漆味 散发 不 出去 更有甚者 因是 围拢 房屋结构 早晚 有人 道 说话 很闹 道 灯笼 整夜 亮 透过 菲薄 窗帘 不得不 忍受 光污染 屋子 隔音 效果 很差 需要 忍受 隔壁 看电视 声音 没有 中央空调 3 服务水平 调换 房间 第二天 讯问 前台 要求 换到 豪华 标间 前台 说 要求 调换 不补 差价 且说 现房 即为 折后 338 元 价位 承德 前台 服务 很差 身着 民族服装 很漂亮 态度 冷冰冰 之前 老外 鞠躬 哈腰 4 早餐 难以 下咽 周围 村庄 只能 酒店 早餐 四五个 小 咸菜 两种 粥 豆包 包子 奶粉 冲得 很 稀 牛奶 这种 早餐 八点半 还 没有 打扫卫生 是否 客人 吃 完 非常 非常 后悔 选择 酒店 性价比 最差 本想 休息 几天 住宿 实在 太差 干脆 一家人 勉强 住 两夜 退房 走人|$27815$376:0.00411522633744856 821:0.00411522633744856 872:0.01234567901234568 942:0.00411522633744856 958:0.00411522633744856 1083:0.00411522633744856 1091:0.00411522633744856 1109:0.00411522633744856 1868:0.00411522633744856 1941:0.020576131687242802 2149:0.01646090534979424 2216:0.00411522633744856 2244:0.00411522633744856 2268:0.00823045267489712 2359:0.00411522633744856 2533:0.00411522633744856 2556:0.00411522633744856 2693:0.00411522633744856 3061:0.00411522633744856 3357:0.00411522633744856 3562:0.00823045267489712 3645:0.01646090534979424 3705:0.00411522633744856 3758:0.00823045267489712 3951:0.00411522633744856 4175:0.01234567901234568 4447:0.00411522633744856 4546:0.00411522633744856 5259:0.00411522633744856 5570:0.00411522633744856 5762:0.00411522633744856 5782:0.00411522633744856 5950:0.00411522633744856 6028:0.00411522633744856 6202:0.00411522633744856 6420:0.00411522633744856 6586:0.00823045267489712 6589:0.00411522633744856 6747:0.00411522633744856 7308:0.00411522633744856 7365:0.00411522633744856 7698:0.00411522633744856 8334:0.00411522633744856 8422:0.00411522633744856 8889:0.00411522633744856 9834:0.00411522633744856 9935:0.01234567901234568 9952:0.00823045267489712 10599:0.00411522633744856 10636:0.00411522633744856 11001:0.00411522633744856 11023:0.00823045267489712 11047:0.00411522633744856 11161:0.00411522633744856 11359:0.00411522633744856 11497:0.00411522633744856 11509:0.00411522633744856 11712:0.00411522633744856 11729:0.00411522633744856 11773:0.00411522633744856 11803:0.00411522633744856 11831:0.00411522633744856 11948:0.00411522633744856 12039:0.00411522633744856 12254:0.01234567901234568 12268:0.00411522633744856 12351:0.00411522633744856 12408:0.00411522633744856 12523:0.00411522633744856 12669:0.00411522633744856 12727:0.00411522633744856 12796:0.00411522633744856 12832:0.00411522633744856 13236:0.00411522633744856 13452:0.00411522633744856 13461:0.00823045267489712 13792:0.00411522633744856 13812:0.00411522633744856 13909:0.00411522633744856 13969:0.00411522633744856 14122:0.00411522633744856 14296:0.01234567901234568 14351:0.00823045267489712 14718:0.00411522633744856 14773:0.00411522633744856 14947:0.00411522633744856 14997:0.00411522633744856 15072:0.00823045267489712 15213:0.00411522633744856 15252:0.00411522633744856 15285:0.00823045267489712 15288:0.00411522633744856 15296:0.00411522633744856 15321:0.00823045267489712 15410:0.00411522633744856 15589:0.00411522633744856 15894:0.00411522633744856 16035:0.00411522633744856 16300:0.00411522633744856 16528:0.00411522633744856 16529:0.00823045267489712 16883:0.00823045267489712 16904:0.00411522633744856 17167:0.00411522633744856 17198:0.00411522633744856 17298:0.00411522633744856 17304:0.00411522633744856 17446:0.00411522633744856 17673:0.00411522633744856 17904:0.00411522633744856 18571:0.00411522633744856 18612:0.00411522633744856 18808:0.00411522633744856 18823:0.00411522633744856 18871:0.00411522633744856 19073:0.00411522633744856 19268:0.00411522633744856 19507:0.00411522633744856 19606:0.00823045267489712 19633:0.00411522633744856 19692:0.00411522633744856 19809:0.00411522633744856 19814:0.00411522633744856 19832:0.00411522633744856 19887:0.00411522633744856 20034:0.00411522633744856 20041:0.00411522633744856 20157:0.00411522633744856 20243:0.00411522633744856 20266:0.00411522633744856 20493:0.00823045267489712 21260:0.00411522633744856 21294:0.00411522633744856 21521:0.01646090534979424 21767:0.00411522633744856 21847:0.00411522633744856 21893:0.00411522633744856 21994:0.00411522633744856 22123:0.00411522633744856 22568:0.00411522633744856 22691:0.00411522633744856 22855:0.00411522633744856 23417:0.00411522633744856 23431:0.00411522633744856 23523:0.00411522633744856 23616:0.00411522633744856 23660:0.00411522633744856 23720:0.00411522633744856 23755:0.00411522633744856 24109:0.00411522633744856 24484:0.00411522633744856 24583:0.00411522633744856 24623:0.00823045267489712 24708:0.00411522633744856 24880:0.00411522633744856 24986:0.00823045267489712 25000:0.00411522633744856 25054:0.00411522633744856 25464:0.00823045267489712 25465:0.00411522633744856 25496:0.00411522633744856 25663:0.00823045267489712 25725:0.00411522633744856 25956:0.00411522633744856 26100:0.00411522633744856 26165:0.00411522633744856 26387:0.00411522633744856 26391:0.00411522633744856 26412:0.00411522633744856 27141:0.00411522633744856 27337:0.00823045267489712 27397:0.00411522633744856 27453:0.00411522633744856 27601:0.00411522633744856 27753:0.00411522633744856|0|{"0":"0.9982921759021707","1":"0.0017078240978293246"}
0|三个字:脏、乱、差!房间里面看上去还可以,但是仔细看,很多地方极其脏,服务人员貌似没有培训上岗的,从前台到餐厅的服务人员都是这样,走廊上停着服务车,24小时没有换地方,四周环境乱得很,和照片完全不相符。饮食很差!|三个 字 脏 乱 差 房间 里面 看上去 还 仔细 看 很多 地方 极其 脏 服务 人员 貌似 没有 培训 上岗 前台 餐厅 服务 人员 都 走廊 上 停 服务车 24 小时 没有 换 地方 四周 环境 乱 很 照片 完全 不 相符 饮食 很差|$27815$474:0.022222222222222223 518:0.022222222222222223 1845:0.022222222222222223 1980:0.022222222222222223 2693:0.022222222222222223 3346:0.022222222222222223 3541:0.022222222222222223 5259:0.044444444444444446 7363:0.022222222222222223 7365:0.022222222222222223 7402:0.022222222222222223 8182:0.022222222222222223 8541:0.022222222222222223 9935:0.044444444444444446 11161:0.022222222222222223 11509:0.044444444444444446 13461:0.022222222222222223 14296:0.022222222222222223 15285:0.022222222222222223 15296:0.022222222222222223 15321:0.022222222222222223 16310:0.022222222222222223 16791:0.022222222222222223 17296:0.022222222222222223 17400:0.022222222222222223 18693:0.022222222222222223 18871:0.044444444444444446 19187:0.022222222222222223 21521:0.022222222222222223 22962:0.022222222222222223 23846:0.022222222222222223 24033:0.044444444444444446 24455:0.044444444444444446 25663:0.022222222222222223 25838:0.022222222222222223 25883:0.022222222222222223 26013:0.022222222222222223 27523:0.022222222222222223|0|{"0":"0.9998637697625883","1":"1.3623023741171636E-4"}
LR
-------------------------------- Metrics: --------------------------------
Auc:0.9035	Accuracy:0.8468	Precision:0.8857	Recall:0.8971	F1:0.8913	LogLoss:0.7402
|Pred\Real|  1|  0|
|---------|---|---|
|        1|488| 63|
|        0| 56|170|

从打印的5条结果数据的列名信息可以看出,原始数据为“label”和“review”两列;进行文本数据处理增加“featureText”列,转换为向量列“featureVector”,后面便是预测结果的两列“pred”和“pred_info”。


使用LocalPredictor的代码如下:

LocalPredictor localPredictor
	= new LocalPredictor(DATA_DIR + "lr_pipeline_model.ak", "review string");

final int index_pred = TableUtil.findColIndex(localPredictor.getOutputSchema(), "pred");

String[] reviews = new String[] {
	"硬件不错,服务态度也不错,下次到附近的话还会选择住这里",
	"房间还比较干净,交通方便,离外滩很近.但外面声音太大,休息不好",
};

for (String review : reviews) {
	Object[] result = localPredictor.predict(review);
	System.out.println("Pred Result : " + result[index_pred] + " @ " + review);
}

注意:预测结果的类型为Row,即为一行数据,包含多个数据字段,在计算过程中,原始数据“review”一列;进行文本数据处理增加“featureText”列,转换为向量列“featureVector”,后面便是预测结果及详情的两列“pred”和“pred_info”。可以通过TableUtil.findColIndex方法,确定预测结果“pred”列的位置索引值index_pred,从Row类型变量result中使用getField方法获取索引值为index_pred的数据字段。


运行结果为:

Pred Result : 1 @ 硬件不错,服务态度也不错,下次到附近的话还会选择住这里
Pred Result : 1 @ 房间还比较干净,交通方便,离外滩很近.但外面声音太大,休息不好