首页
/ ML-For-Beginners NLP 模块实战:从对话机器人到欧洲 51.5 万条酒店评论的情感分析

ML-For-Beginners NLP 模块实战:从对话机器人到欧洲 51.5 万条酒店评论的情感分析

2026-09-06 16:15:51作者:劳婵绚Shirley

本文围绕 ML-For-Beginners 课程中 6-NLP 自然语言处理模块 展开,完整梳理该模块的教学主线:以简·奥斯汀《傲慢与偏见》文本和欧洲酒店评论数据集为两条实战线索,依次学习 NLP 任务体系、机器翻译、pandas 数据探查,最终用 NLTK 的 VADER 完成大规模评论的情感分析流水线。读完并动手完成后,你将能够独立搭建一个带情感感知的对话机器人,并对 51.5 万行的真实评论数据完成“清洗—标签特征化—情感打分—落盘复用”的完整 NLP 数据工程。

《傲慢与偏见》书籍与茶具,NLP 模块的文学主题配图

模块概览:文学主题下的 5 课学习路径

自然语言处理(Natural Language Processing, NLP)指计算机程序理解口头的和书面的自然语言的能力,是人工智能的组成部分。据模块首页介绍,NLP 已存在 50 多年,根植于语言学领域,目标是让机器理解并处理人类语言,进而完成拼写检查、机器翻译等任务,在医学研究、搜索引擎和商业智能等方向都有落地应用。

该模块选定了区域化主题:欧洲语言与文学 + 欧洲浪漫风情的酒店。课程先通过和简·奥斯汀 1813 年出版的长篇小说《傲慢与偏见》(Pride and Prejudice)中伊丽莎白·班内特与达西先生“对话”来学习 NLP 基础,再用欧洲的酒店预订评论学习情感分析(sentiment analysis)。全部课程由 Stephen Howell 编写,共 5 课:

课次 主题 文档入口 配套练习
1 自然语言处理入门 6-NLP/1-Introduction-to-NLP/README.md Search for a bot
2 常见 NLP 任务与技术 6-NLP/2-Tasks/README.md Make a bot talk back
3 机器翻译与情感分析 6-NLP/3-Translation-Sentiment/README.md Poetic license
4 酒店评论数据准备与探查 6-NLP/4-Hotel-Reviews-1/README.md NLTK
5 NLTK 情感分析(VADER) 6-NLP/5-Hotel-Reviews-2/README.md Try a different dataset

每课均配有课前/课后测验(quiz)和动手练习,第 1、2 课的练习解可直接参考仓库中的 bot.py 参考实现,第 4、5 课配有可运行的 notebook 解(如 4 课解 notebook5 课标签处理 notebook5 课情感分析 notebook)。

第 1 课:NLP 的起源与第一个对话机器人

计算语言学与“听懂”的难点

NLP 是计算语言学(computational linguistics)的子领域,研究计算机如何工作于、理解、翻译并与人类语言通信。一个直观的例子是手机听写:你说的话先被转成文本,再被解析(parse),检测出的关键词被转成手机或虚拟助手可执行的结构。课程同时提醒:让计算机真正“理解”句子的意义非常困难,尤其是理解幽默、检测反讽这类情绪。语法层面(名词、动词、简单现在时与现在进行时)反而是计算机的强项——因为计算机擅长套用形式化规则;真正难的挑战在于理解句子的含义情感倾向

从图灵测试到 Eliza

  • 图灵测试:20 世纪 50 年代,Alan Turing 设想通过文字往来让人类对话者判断对方是人还是机器;若人类无法分辨,机器是否可以说成在“思考”?
  • 灵感来源:这一设想源自“模仿游戏”(The Imitation Game)聚会游戏——审问者通过传纸条的方式,依据书面回答判断另一房间中两人的性别。
  • Eliza:1960 年代 MIT 科学家 Joseph Weizenbaum 开发了“计算机治疗师”Eliza。它能解析句子、识别某些语法结构和关键词并给出貌似合理的回答,但并不理解句子。例如对它说 “I am sad”,它可能改写成 “How long have you been sad”——只是变换了时态、替换了若干词;若写 “You are a bicycle”,它可能反问 “How long have I been a bicycle?”。Eliza 很容易被套路,找不到关键词时则给出一个对多种陈述都适用的随机回答。

动手练习:构建 Marvin 机器人

本练习要求用 Python 写一个只具备“随机回应、把对话继续下去”这一种能力的机器人,步骤为:

  1. 打印交互说明;
  2. 进入循环:接受输入 → 若用户要求退出则退出 → 处理输入并决定响应(本例为从通用随机回复中随机取一条)→ 打印响应;
  3. 回到步骤 2。

首先定义随机回复列表:

random_responses = ["That is quite interesting, please tell me more.",
                    "I see. Do go on.",
                    "Why do you say that?",
                    "Funny weather we've been having, isn't it?",
                    "Let's change the subject.",
                    "Did you catch the game last night?"]

期望的样例输出(以 > 开头的是用户输入):

Hello, I am Marvin, the simple robot.
You can end this conversation at any time by typing 'bye'
After typing each answer, press 'enter'
How are you today?
> I am good thanks
That is quite interesting, please tell me more.
> today I went for a walk
Did you catch the game last night?
> I did, but my team lost
Funny weather we've been having, isn't it?
> bye
It was nice talking to you, goodbye!

从仓库的参考实现 1 课 bot.py 可以看到核心结构:while True 循环中用 input("> ") 读入,user_input.lower() == "bye"break,否则用 random.choices(random_responses)[0] 取一条回复——与练习计划一一对应。课后“停下来想一想”要求你考虑:随机回复能否骗到人?机器人需要哪些特性才更有效?如果机器人真正“理解”句子含义,是否也需要“记住”上下文?

环境要求:本课代码示例基于 Python 3.8,建议使用 Visual Studio Code + Python 扩展(或任意 Python IDE)。文本处理依赖 TextBlob,一个简化版 Python 文本处理库,安装命令为:

pip install -U textblob
python -m textblob.download_corpora

第二条命令用于下载 TextBlob 依赖的语料库,后续课的命名实体与情感功能都依赖这些 corpora。

第 2 课:NLP 标准任务清单与 TextBlob 实战

NLP 专家常遇到的任务

绝大多数 NLP 任务都需要先把文本切分、检查,再与规则和数据集交叉对照。课程给出的标准任务清单如下:

  • Tokenization(分词):把文本切成词(tokens)。看似简单,但要处理标点与不同语言的词句边界,并不轻松;
  • Embeddings(词嵌入):把文本转成数值,使语义相近或经常共现的词在空间中聚簇(例如 'toy' 会与 'disney'、'lego' 等聚类);
  • Parsing & Part-of-speech Tagging(解析与词性标注):如 the quick red fox jumped over the lazy brown dog 中 fox = 名词、jumped = 动词;解析还要识别句子中词语的关联结构(the quick red fox jumpedthe lazy brown dog 是独立的两段结构);
  • Word and Phrase Frequencies(词/短语频率):对 Kipling 诗《The Winners》的示例节选中,a friend 出现 2 次、the 出现 6 次、travels 出现 2 次;
  • N-grams(N 元组):把文本切成固定长度的词序列,如 bigram 对 the quick red fox jumped over the lazy brown dog 会产生 the quickquick red 直至 brown dog 共 9 个二元组,可直观想象为在句子上滑动的窗口;
  • Noun phrase Extraction(名词短语抽取):英文中主语/宾语前常有 a/an/the,是理解句意的常用抓手;
  • Sentiment analysis(情感分析):以极性(polarity,-1.0 到 1.0)与客观/主观度(objectivity/subjectivity,0.0 到 1.0)度量文本情感;
  • Inflection(屈折变化):取得单词的单数/复数形式;
  • Lemmatization(词形还原):取词根,如 flewfliesflying 的词根是动词 fly
  • WordNet:收录多语言词汇、同义词、反义词等细节的数据库,对构建翻译器、拼写检查器非常有价值。

用 TextBlob 抽取名词短语

TextBlob “站在 NLTK 与 pattern 这两个巨人的肩膀上”,API 内嵌了相当多的机器学习能力。识别名词短语时它提供多种抽取器,课程示例使用 ConllExtractor

from textblob import TextBlob
from textblob.np_extractors import ConllExtractor
# import and create a Conll extractor to use later
extractor = ConllExtractor()

# later when you need a noun phrase extractor:
user_input = input("> ")
user_input_blob = TextBlob(user_input, np_extractor=extractor)  # note non-default extractor specified
np = user_input_blob.noun_phrases

其背后的原理:ConllExtractor 是用 ConLL-2000 训练语料训练出的分块(chunking)解析器——2000 年的 Computational Natural Language Learning 会议以“名词分块”为专题任务,模型在《华尔街日报》第 15–18 节(211727 个 tokens)上训练、第 20 节(47377 个 tokens)上测试。

挑战:让 Marvin 具备“同理心”

在上一课随机回复机器人基础上,本任务要求:根据输入的情感极性选择 4 档回应,若检测到名词短语则将其复数化并追问该主题:

if user_input_blob.polarity <= -0.5:
  response = "Oh dear, that sounds bad. "
elif user_input_blob.polarity <= 0:
  response = "Hmm, that's not great. "
elif user_input_blob.polarity <= 0.5:
  response = "Well, that sounds positive. "
elif user_input_blob.polarity <= 1:
  response = "Wow, that sounds great. "

期望样例输出:

How are you today?
> I am ok
Well, that sounds positive. Can you tell me more?
> I went for a walk and saw a lovely cat
Well, that sounds positive. Can you tell me more about lovely cats?
> cats are the best. But I also have a cool dog
Wow, that sounds great. Can you tell me more about cool dogs?
> bye
It was nice talking to you, goodbye!

仓库中的完整实现 2 课 bot.py 展示了关键细节:模块级创建 extractor = ConllExtractor(),循环内用 TextBlob(user_input, np_extractor=extractor) 构造 blob;当 len(np) != 0 时用 np[0].pluralize() 复数化首个名词短语(cat → cats、mouse → mice)拼进追问句,否则退化为通用的 “Can you tell me more?”。这个文件是把第 1 课的骨架与第 2 课的 NLP 能力组合在一起的最佳参照。

第 3 课:机器翻译与情感极性

为什么翻译是难题:英语—爱尔兰语示例

翻译的难点在于各语言语法规则差异巨大。课程用英语→爱尔兰语(Irish)的例子说明:英语 I feel happy 的词序是主语 (I)—动词 (feel)—形容词 (happy);而爱尔兰语中情绪是“加诸于你身上”的,对应 Tá athas orm,字面直译是 “Happy is upon me”,其正式语序为动词 (Tá or is)—形容词 (athas)—主语 (orm)。母语者说英语时会说 I feel happy 而非直译,因为他们理解意义,而不只是搬词语和句序。

naive(逐词直译)程序会产出错误译文:I feel happy 直译成 Mise bhraitheann athas,字面是 “me feel happy”,根本不是合法的爱尔兰语句子——尽管英语和爱尔兰语同处两个相邻的岛屿。

机器学习路径:如果你有源语言和目标语言的大量平行文本(corpus/corpora),就可以忽略词义、让模型检测模式。以《傲慢与偏见》的英法对照为例,可以发现被“惯用地”翻译的短语对。另一个细节是“伪友词”:英语 I have no money 直译成法语可能是 Je n'ai pas de monnaie,但 monnaie 在法语里是“零钱”,人译者会选择 Je n'ai pas d'argent。当模型有足够多的人类翻译可学时,它能通过识别被双语专家反复采用的模式来提升翻译准确性。

用 TextBlob 可以真实地体验机器翻译——试译《傲慢与偏见》的第一句话:

from textblob import TextBlob

blob = TextBlob(
    "It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife!"
)
print(blob.translate(to="fr"))

TextBlob 的译法:“C'est une vérité universellement reconnue, qu'un homme célibataire en possession d'une bonne fortune doit avoir besoin d'une femme!”。课程指出它甚至比 1932 年 V. Leconte 与 Ch. Pressoir 的法文译本更贴合原文(原译本为“清晰”添加了原文没有的表述)。背后机制:blob.translate 走的是 Google Translate,一个能解析海量短语并预测最佳译串的 AI 服务——全程无手工规则,且使用它需要联网

情感极性实验:用《傲慢与偏见》原文打分

情感以 polarity(-1 到 1)和 subjectivity(0 客观—1 主观)两个维度度量。课程给了一个直接可跑的示例,分析书中首句和末句:

from textblob import TextBlob

quote1 = """It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife."""

quote2 = """Darcy, as well as Elizabeth, really loved them; and they were both ever sensible of the warmest gratitude towards the persons who, by bringing her into Derbyshire, had been the means of uniting them."""

sentiment1 = TextBlob(quote1).sentiment
sentiment2 = TextBlob(quote2).sentiment

print(quote1 + " has a sentiment of " + str(sentiment1))
print(quote2 + " has a sentiment of " + str(sentiment2))

输出:

It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife.
has a sentiment of Sentiment(polarity=0.20952380952380953, subjectivity=0.27142857142857146)

Darcy, as well as Elizabeth, really loved them; and they were
both ever sensible of the warmest gratitude towards the persons
who, by bringing her into Derbyshire, had been the means of
uniting them.
has a sentiment of Sentiment(polarity=0.7, subjectivity=0.8)

该课的挑战是:下载全书文本(去头尾元数据),用循环逐句分析,把极性为 1 或 -1 的句子分别存入正/负列表,最后打印两组句子及数量,判断全书“绝对正面”句是否多于“绝对负面”句,参考解在 3 课 solution notebook。知识检查部分特意列出多条被判为绝对正面的句子(例如 “Happy shall I be, when his stay at Netherfield is over!” 实为反话、 “Our distress, my dear Lizzy, is very great.” 实为负面),引导你思考基于词典词频的情感算法为何会误判——奥斯汀笔下人物大量使用反讽与双关,这正是简单极性算法的盲区。

第 4 课:51.5 万条欧洲酒店评论数据探查

数据集与列结构

本阶段进入真实数据集:515K Hotel Reviews Data in Europe(约 515,738 行 × 17 列,解压后约 230 MB),采用 CC0 公共领域许可,从 Booking.com 公开数据抓取,数据集创建者为 Jiashen Liu。按课程要求,下载后放到本 NLP 课程的 data 目录(即仓库中的 6-NLP/data 目录,对应各课代码中的相对路径 ../../data/Hotel_Reviews.csv)。数据覆盖 6 个城市、1493 家酒店,目标是构建一个用情感分析与游客评分做酒店推荐的 bot,可回答的问题包括:评论中最常用的词和短语是什么?酒店官方 Tags 是否与评分相关(如 Family with young children 是否比 Solo traveller 打更低的分)?NLTK 情感得分与游客数字评分是否“一致”?

17 个表头按语义可分为四组(引自 4 课文档):

  • 酒店列Hotel_NameHotel_Addresslatlng。经纬度可用于画酒店位置地图;地址对建模不直观,后续会替换为城市+国家;
  • 酒店元评分列Average_Score(数据集创建者说明是“基于过去一年最新评论计算”的酒店平均分,计算口径 unusual,先按面值取用)、Total_Number_of_ReviewsAdditional_Number_of_Scoring(打了分但未写正负评的评分数);
  • 评论列Reviewer_Score(2.5–10、最多一位小数的数值,2.5 即为最低分)、Negative_Review / Positive_Review(无内容时为 "No Negative" / "No Positive";且可能出现负评列里写好评、正评列里写差评的错位情况)、Review_Total_Negative_Word_Counts / Review_Total_Positive_Word_Counts(不检查语义时,字数可粗略暗示倾向)、Review_Datedays_since_review(新鲜度)、Tags(客人类型/房型/入住时长/提交设备等 5–6 个逗号分隔值);
  • 评审人列Total_Number_of_Reviews_Reviewer_Has_Given(无唯一 ID,无法把同一人的多条评论关联起来,建模价值有限)、Reviewer_Nationality(课程特别提醒:不要把“某国籍更爱给高分/低分”这类刻板印象带进模型,每条评分都是个体基于自身经历打出的)。

Tags 的麻烦:标签未标准化——同一家酒店可能是 Single room / Twin room,另一家却是 Deluxe Single Room / Classic Queen Room;且字段顺序不定(有人留空导致错位)。统计 Type of group 有 1025 种取值,仅过滤含 family 的结果还混入大量 Family room 房型词;加入 with 一词后,515,000 条中含 “Family with young children” 或 “Family with older children” 的超过 80,000 条。结论:Tags 不是完全没用,但要花功夫才可用。

Average_Score 的疑点:评论数最多的 Britannia International Hotel Canary Wharf 在数据集中有 4789 条评论,但 Total_Number_of_Reviews 是 9086;加上 Additional_Number_of_Scoring(2682)得 7471,仍差 1615。该酒店 Average_Score 为 7.1,而用数据集内评分自行计算得 6.8——只能猜测差异来自我们拿不到的那部分评分。第二高评论数酒店的自算均分 8.12 与给定值 8.1 却吻合。因此课程结论是:无法验证的字段(Average_ScoreAdditional_Number_of_ScoringTotal_Number_of_Reviews)不可盲信,应自行计算。

课程还附了一段重要提醒:后续代码会自动“读”负面评论而无需人工阅读——有些差评与酒店无关(“天气不好”),有些甚至带有种族、性别或年龄歧视色彩。这是公开网站抓取数据集的暗面,让代码去度量情感,而不是亲自读每一条

用 pandas 验证数据:加载与 9 个探查任务

加载代码(注意记录加载耗时):

# Load the hotel reviews from CSV
import pandas as pd
import time
print("Loading data file now, this could take a while depending on file size")
start = time.time()
df = pd.read_csv('../../data/Hotel_Reviews.csv')
end = time.time()
print("Loading took " + str(round(end - start, 2)) + " seconds")

课程随后布置 9 个“先自己写、再对照”的编码任务,并给出了关键结果:

  1. 数据形状:The shape of the data (rows, cols) is (515738, 17)
  2. 国籍频率:df["Reviewer_Nationality"].value_counts()227 种国籍,United Kingdom 以 245,246 条居首,其后 10 位为 United States of America (35437)、Australia (21686)、Ireland (14827)、United Arab Emirates (10235)、Saudi Arabia (8951)、Netherlands (8772)、Switzerland (8678)、Germany (7941)、Canada (7894)、France (7296)。注意值前导空格,打印用 strip() 去除;
  3. 前 10 大国籍各自最常评论的酒店(用 df[df["Reviewer_Nationality"] == nat] 过滤再 value_counts()),例如英国的 Britannia International Hotel Canary Wharf(3833 条)、美国的 Hotel Esther a(423 条)。课程明确提示:这种显式循环写法不适合大数据量,仅作演示;
  4. 每家酒店在数据集中的实际评论数:用 groupby('Hotel_Name').transform('count') 生成 Total_Reviews_Found 并去重,可看到 Britannia 自算 4789 条 vs Total_Number_of_Reviews 9086,印证前述口径问题——该列因此不进入后续模型;
  5. 自算平均分:df['Calc_Average_Score'] = round(df.groupby('Hotel_Name').Reviewer_Score.transform('mean'), 1),再用 df.apply(get_difference_review_avg, axis=1) 算差值排序。最大偏差只有 1 家酒店差超过 1 分(Kube Hotel Ice Bar:7.2 vs 5.9),其余多在 ±0.8 内,可放心用自算均值替代; 6–8. 统计 “No Negative” / “No Positive” / 两者同时出现的行数。

性能对比是本课的一个亮点。lambda 逐行 apply 的写法结果为:No Negative 127,890 行、No Positive 35,946 行、两者都是 127 行,耗时 9.64 秒;换成布尔表达式求和:

no_negative_reviews = sum(df.Negative_Review == "No Negative")
no_positive_reviews = sum(df["Positive_Review"] == "No Positive")
both_no_reviews = sum((df.Negative_Review == "No Negative") & (df.Positive_Review == "No Positive"))

同样结果只花 0.19 秒——50 倍以上的差距。那 127 行(占 0.02%)是只打分、正负评都空白的记录,量级太小不会扭曲模型,但这类“意料之外”的行正是数据探查的价值所在。完整过程可对照 4 课解 notebook

第 5 课:过滤、标签特征化与 VADER 情感分析

数据清洗:地址归一化与自算列替换

清洗分四步(详见 5 课文档):

  1. 酒店列:丢弃 latlng;用字符串包含判断把 Hotel_Address 归一化为 6 个“城市, 国家”:
def replace_address(row):
    if "Netherlands" in row["Hotel_Address"]:
        return "Amsterdam, Netherlands"
    elif "Barcelona" in row["Hotel_Address"]:
        return "Barcelona, Spain"
    elif "United Kingdom" in row["Hotel_Address"]:
        return "London, United Kingdom"
    elif "Milan" in row["Hotel_Address"]:
        return "Milan, Italy"
    elif "France" in row["Hotel_Address"]:
        return "Paris, France"
    elif "Vienna" in row["Hotel_Address"]:
        return "Vienna, Austria"

df["Hotel_Address"] = df.apply(replace_address, axis = 1)
print(df["Hotel_Address"].value_counts())

归一化后即可按城市聚合酒店数:df.groupby("Hotel_Address").agg({"Hotel_Name": "nunique"}),结果为 Amsterdam 105、Barcelona 211、London 400、Milan 162、Paris 458、Vienna 158。 2. 元评分列:丢弃 Additional_Number_of_ScoringTotal_Number_of_Reviews 用数据集中该酒店的实际行数替换(groupby('Hotel_Name').transform('count'));Average_Score 用自算均值替换(round(groupby(...).Reviewer_Score.transform('mean'), 1))。 3. 评论列:丢弃 Review_Total_Negative_Word_CountsReview_Total_Positive_Word_CountsReview_Datedays_since_review;保留 Reviewer_ScoreNegative_ReviewPositive_ReviewTags 先留着,下一步处理。 4. 评审人列:丢弃 Total_Number_of_Reviews_Reviewer_Has_Given,保留 Reviewer_Nationality

Tags 特征工程:从 2428 个杂项标签到 8 个可用特征

Tags 列以文本形式存储了一个列表,例如 [' Business trip ', ' Solo traveler ', ' Single Room ', ' Stayed 5 nights ', ' Submitted from a mobile device '],顺序与数量不固定,全表约 676 万个词,直接做多词频率统计成本极高。课程先用 pandas 字符串方法快速规整:

# Remove opening and closing brackets
df.Tags = df.Tags.str.strip("[']")
# remove all quotes too
df.Tags = df.Tags.str.replace(" ', '", ",", regex = False)

由于标签顺序不定,不能按“第几个逗号前”取值;课程的做法是利用“每个标签都是多词短语且被逗号分隔”这一点:把 6 个临时列按出现顺序展开、合并成一个大列后跑 value_counts(),得到 2428 个唯一标签,高频样本包括 Leisure trip (417,778)、Submitted from a mobile device (307,640)、Couple (252,294)、Stayed 1 night (193,645)、Solo traveler (108,545) 等。

然后按“推荐酒店”这一目标做取舍:行程类型(保留)、客人组别(保留)、房型/套房(丢弃,各家酒店房间大同小异)、提交设备(丢弃)、入住晚数(“可能”相关但牵强,丢弃)。剔除 “Stayed N nights” 与房型类标签后,剩下 8 个有用标签:

标签 计数
Leisure trip 417778
Couple 252294
Solo traveler 108545
Business trip 82939
Group(并入 Travellers with friends) 67535
Family with young children 61015
Family with older children 26349
With a pet 1405

识别这些标签的完整代码见 5 课标签处理 notebook。最后把每个标签变成 0/1 二值列,聚合后即可回答“该酒店更适合商务还是休闲、是否常被带宠物”:

# Process the Tags into new columns
df["Leisure_trip"] = df.Tags.apply(lambda tag: 1 if "Leisure trip" in tag else 0)
df["Couple"] = df.Tags.apply(lambda tag: 1 if "Couple" in tag else 0)
df["Solo_traveler"] = df.Tags.apply(lambda tag: 1 if "Solo traveler" in tag else 0)
df["Business_trip"] = df.Tags.apply(lambda tag: 1 if "Business trip" in tag else 0)
df["Group"] = df.Tags.apply(lambda tag: 1 if "Group" in tag or "Travelers with friends" in tag else 0)
df["Family_with_young_children"] = df.Tags.apply(lambda tag: 1 if "Family with young children" in tag else 0)
df["Family_with_older_children"] = df.Tags.apply(lambda tag: 1 if "Family with older children" in tag else 0)
df["With_a_pet"] = df.Tags.apply(lambda tag: 1 if "With a pet" in tag else 0)

处理完成后,丢弃剩余的字数统计与评审人历史列,把结果另存:

df.drop(["Review_Total_Negative_Word_Counts", "Review_Total_Positive_Word_Counts",
         "days_since_review", "Total_Number_of_Reviews_Reviewer_Has_Given"], axis = 1, inplace=True)
print("Saving results to Hotel_Reviews_Filtered.csv")
df.to_csv(r'../data/Hotel_Reviews_Filtered.csv', index = False)

去停用词 + VADER 情感分析

情感分析阶段的加载与依赖如下(VADER 词典需下载):

import time
import pandas as pd
import nltk as nltk
from nltk.corpus import stopwords
from nltk.sentiment.vader import SentimentIntensityAnalyzer
nltk.download('vader_lexicon')

# Load the filtered hotel reviews from CSV
df = pd.read_csv('../../data/Hotel_Reviews_Filtered.csv')

去停用词:在测试机上直接对两列做情感分析需 12–14 分钟(取决于情感库),值得优化。停用词(不影响句意情感的常见英文词)先剔除:最长一条负评从 395 词降到 195 词,全表 51.5 万行两列去停用词仅花 3.3 秒——用极小成本换后续提速:

from nltk.corpus import stopwords

cache = set(stopwords.words("english"))
def remove_stopwords(review):
    text = " ".join([word for word in review.split() if word not in cache])
    return text

df.Negative_Review = df.Negative_Review.apply(remove_stopwords)
df.Positive_Review = df.Positive_Review.apply(remove_stopwords)

(把停用词表放进 set 而非 list,是查找从 O(n) 到 O(1) 的关键,对应课程引用的 Kaggle 快速去停用词做法。)

VADER 打分:NLTK 提供多种可替换的情感分析器,本课选用 VADER(Hutto & Gilbert, 2014,面向社交媒体文本的规则型模型)。打分函数处理三种输入——“No Negative”/“No Positive” 返回 0,否则返回 compound 分数:

from nltk.sentiment.vader import SentimentIntensityAnalyzer

vader_sentiment = SentimentIntensityAnalyzer()

def calc_sentiment(review):
    if review == "No Negative" or review == "No Positive":
        return 0
    return vader_sentiment.polarity_scores(review)["compound"]

应用到两列:

print("Calculating sentiment columns for both positive and negative reviews")
start = time.time()
df["Negative_Sentiment"] = df.Negative_Review.apply(calc_sentiment)
df["Positive_Sentiment"] = df.Positive_Review.apply(calc_sentiment)
end = time.time()
print("Calculating sentiment took " + str(round(end - start, 2)) + " seconds")

课程实测约 120 秒(因机器而异)。验证思路是拿情感分对照 Reviewer_Score:如果负评被判为极性 1(极正面)而游客打最低分,要么文本与评分不符,要么分析器没读懂——课程明言应预期一些明显错误的分数,且通常可解释,最典型就是反讽(“Of course I LOVED sleeping in a room with no heating”)。

最后重排列并落盘(列顺序纯为便于人工浏览):

df = df.reindex(["Hotel_Name", "Hotel_Address", "Total_Number_of_Reviews", "Average_Score",
                 "Reviewer_Score", "Negative_Sentiment", "Positive_Sentiment",
                 "Reviewer_Nationality", "Leisure_trip", "Couple", "Solo_traveler",
                 "Business_trip", "Group", "Family_with_young_children",
                 "Family_with_older_children", "With_a_pet",
                 "Negative_Review", "Positive_Review"], axis=1)
print("Saving results to Hotel_Reviews_NLP.csv")
df.to_csv(r"../data/Hotel_Reviews_NLP.csv", index = False)

完整流水线回顾

模块在 5 课文档 末尾把四步串成完整链路,这也是本模块最值得带走的方法论:

  1. 原始数据 Hotel_Reviews.csv4 课解 notebook 完成探索;
  2. 过滤/特征化(1-notebook)产出 Hotel_Reviews_Filtered.csv
  3. 情感分析(3-notebook)产出 Hotel_Reviews_NLP.csv
  4. Hotel_Reviews_NLP.csv 完成最终挑战——课程建议尝试用学过的其他策略(例如聚类)挖掘情感数据中的模式,形成酒店推荐 bot。

小结:这一模块教给你的可迁移能力

  • 概念层:tokenization、embeddings、POS 标注、n-grams、名词短语抽取、词形还原、WordNet 等 NLP 任务清单及其在翻译/情感问题上的局限(反讽、伪友词、跨语言语序);
  • 工具层:TextBlob(sentimentnoun_phrasespluralizetranslate,及其背后的 ConLL-2000 分块模型与在线翻译服务)与 NLTK(stopwords、VADER polarity_scores)两套库的正确安装与语料下载方式;
  • 数据工程层:面对 50 万行带脏口径字段(Average_Score/Total_Number_of_Reviews 无法自证)的真实数据集,如何“自算替代 + 频率统计 + 字符串特征化”把它变成可建模资产;以及 pandas 中 apply(lambda) 与向量化布尔求和之间一个数量级以上的性能差异。

若按顺序完成 1 课2 课3 课4 课5 课 及其各自 assignment,你就同时完成了从“会写一个 bot”到“能把 NLP 特征装进真实推荐系统数据”的完整过渡。

登录后查看全文
热门项目推荐
相关项目推荐