ML-For-Beginners 实战课:基于酒店评论数据的 NLTK 情感分析全流程(数据过滤与 VADER 实现)
导读
本篇文章是开源课程 ML-For-Beginners 第 6 单元 NLP(自然语言处理)中“酒店评论情感分析”课(6-NLP/5-Hotel-Reviews-2)的完整技术展开。围绕约 51.5 万条欧洲酒店真实评论数据,我们将系统地讲解两件事:一是对原始数据集做深度的列级清洗与筛选(把不可信的元数据列换成我们亲手算出来的可靠指标,并把高度冗余的 Tags 列转化为 8 个 0/1 的“旅客画像”列);二是使用 NLTK 完成文本预处理与 VADER 情感分析,为每条评论的正面/负面文本打分,产出可直接用于后续建模与推荐系统的 Hotel_Reviews_NLP.csv。读完本文,你不仅能在自己的机器上复现这条“原始 CSV → 过滤数据 → 情感打分数据”的完整流水线,还能理解每步清洗决策背后的数据动机与性能考量。
说明:本课配套代码均可从仓库获取——英文原版课程见 6-NLP/5-Hotel-Reviews-2/README.md(本篇文章对应的阿拉伯语译文为 translations/ar/6-NLP/5-Hotel-Reviews-2/README.md),完整可执行实现为
solution目录下的三个 Notebook:过滤 1-notebook.ipynb、Tags 分析 2-notebook.ipynb、情感分析 3-notebook.ipynb;除 Python 外,同一套处理思路还提供了 Julia 与 R 两个版本供对比学习。
一、为什么要“再清洗一遍”:原始数据集的可信度问题
本课的数据来自上一课《探索酒店评论数据集》中用到的原始文件 Hotel_Reviews.csv。上一课完成的是宏观的数据探查(EDA),而本课的目标非常具体:
- 把列过滤干净——去掉无信息量的列,修正不可信的列;
- 把 Tags 列变得有用——把文本形式的标签列表转化为可查询、可统计的指标列;
- 引入 NLP 情感分析——对评论的正负文本自动打分,形成新的特征。
正如课程文档所指出的,探索阶段暴露了原始数据集的若干“硬伤”:部分列塞满了无用信息;另一些列虽然看起来正确,但无法弄清其计算方法,也就无法用我们自己的计算独立验证其正确性。因此,与其盲目信任这些既有列,不如全部替换为经过自己计算、口径明确的指标——这正是本课过滤环节的设计初衷。
需要先提醒:Hotel_Reviews.csv 原始数据文件体积较大,并不随仓库提交;需要按 6-NLP/data/README.md 的说明自行下载放置到 6-NLP/data/ 目录后,再运行各 Notebook(Notebook 内部通过相对路径 ../../data/ 读取数据)。
二、第一大步:更彻底的数据清洗
2.1 初始列处理(地址规范化与无用列删除)
数据集中 lat、lng 两列对“酒店推荐”目标无直接帮助,第一步直接删除:
df.drop(["lat", "lng"], axis = 1, inplace=True)
接着处理 Hotel_Address。原始地址列包含街道级别的长文本,但本课的分析只需要“城市 + 国家”粒度。数据集中的城市只有六个:阿姆斯特丹(荷兰)、巴塞罗那(西班牙)、伦敦(英国)、米兰(意大利)、巴黎(法国)、维也纳(奥地利)。通过逐行匹配关键字,把地址统一压缩成这六种取值:
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"
# Replace all the addresses with a shortened, more useful form
df["Hotel_Address"] = df.apply(replace_address, axis = 1)
# The sum of the value_counts() should add up to the total number of reviews
print(df["Hotel_Address"].value_counts())
值得注意的是,在 1-notebook.ipynb 的实现里,replace_address 还包含一个 else 分支直接返回原值,作为地址未能命中六个城市时的兜底,保证函数对每一行都有返回值。
地址规范化之后,就可以用 groupby 直接查询“各城市拥有多少家不同酒店”了:
display(df.groupby("Hotel_Address").agg({"Hotel_Name": "nunique"}))
Hotel_Name 的 nunique(不重复计数)得到各城市酒店数量如下:
| Hotel_Address | Hotel_Name |
|---|---|
| Amsterdam, Netherlands | 105 |
| Barcelona, Spain | 211 |
| London, United Kingdom | 400 |
| Milan, Italy | 162 |
| Paris, France | 458 |
| Vienna, Austria | 158 |
注意六个数字相加恰为 1494,并不等于课程中提到的 1427 家酒店总数,因为同一城市不同分店可能共享地址,且去重口径不同——这正好体现了“亲自用 value_counts() / nunique() 验证”的价值:一切指标都以你的实际数据为准。
2.2 酒店元评论列的处理(用自己算的指标替换“官方”列)
处理对象是描述酒店整体状况的三列元数据:
- 删除
Additional_Number_of_Scoring:其口径不透明且本课用不到; - 替换
Total_Number_of_Reviews:原来表示酒店“累计收到的评论总数”,本课改为该酒店在当前数据集中实际存在的评论行数; - 替换
Average_Score:原来可能是平台全量数据的平均分,本课改为基于本数据集内每条Reviewer_Score重新计算的均值。
实现非常简洁,核心是 pandas 的 groupby + transform——transform 会把聚合结果广播回原始行数,保证不改变 DataFrame 形状:
# Drop `Additional_Number_of_Scoring`
df.drop(["Additional_Number_of_Scoring"], axis = 1, inplace=True)
# Replace `Total_Number_of_Reviews` and `Average_Score` with our own calculated values
df.Total_Number_of_Reviews = df.groupby('Hotel_Name').transform('count')
df.Average_Score = round(df.groupby('Hotel_Name').Reviewer_Score.transform('mean'), 1)
这里 groupby('Hotel_Name').transform('count') 统计的是每个酒店名出现的行数,即本数据集内真实评论条数;transform('mean') 计算同一酒店下 Reviewer_Score 的平均值,并用 round(..., 1) 保留一位小数,从而得到一个口径完全可控、可复算的“平均分”。这种“官方列只做参考、核心指标自己重算”的思路,是数据工程中应对不可信来源数据的基本功。
2.3 评论内容列与评论者列的处理
对评论正文相关的列:
- 删除
Review_Total_Negative_Word_Counts、Review_Total_Positive_Word_Counts(单词计数字段可由文本重新计算,且与后续 NLP 目标无关); - 删除
Review_Date与days_since_review(时间维度在本课不参与分析); - 保留
Reviewer_Score、Negative_Review、Positive_Review三列原样不动——它们分别是评分与正负评论文本,是后续情感分析的主战场; - 暂时保留
Tags列——下一步还要对它做专门的过滤操作,之后才会被删除。
对评论者相关的列:
- 删除
Total_Number_of_Reviews_Reviewer_Has_Given(评论者历史行为在本课目标下无关紧要); - 保留
Reviewer_Nationality(国籍可作后续分析的维度之一)。
在 1-notebook.ipynb 中,这些删除是在生成标签列之后一次性完成的(该 Notebook 还会把 Review_Date 一并删除),随后打印提示并保存,整个过滤过程实测约耗时 23.74 秒。
三、Tags 列:从“文本列表垃圾场”到 8 个有用特征
3.1 Tags 列为什么是难题
Tags 列本质是一个以字符串形式存储的 Python 列表,例如某行可能是 [' Business trip ', ' Solo traveler ', ' Single Room ', ' Stayed 5 nights ', ' Submitted from a mobile device ']。它有三个致命麻烦:
- 它是文本形态的列表,且带方括号、引号和多余空格;
- 每行包含的子标签个数与顺序都不固定——有的行 5 个、有的 3 个、有的 6 个;
- 面对 51.5 万行(课程文档按 515,000 计,实测过滤输出为 515,738 行)× 1427 家酒店,人工根本无法逐个判断哪些短语值得关注;即便用机器直接跑“多词短语频次分布”,对全量 6,762,646 个单词做无差别统计也会慢得离谱。
这正是“探索性数据分析 + NLP 思维”发光的地方:先抽样看几条 Tags,很快就能意识到——标签本来就是已被人工整理好的多词短语,只是以逗号分隔混在列表文本里;我们根本不需要做复杂的短语抽取,只需要做格式清理和结构拆分。
3.2 格式清理与“借位”拆列
首先用最快的 pandas 字符串方法去掉方括号与引号,让每行变成类似 Business trip, Solo traveler, Single Room, Stayed 5 nights, Submitted from a mobile device 的纯逗号分隔串:
# Remove opening and closing brackets
df.Tags = df.Tags.str.strip("[']")
# remove all quotes too
df.Tags = df.Tags.str.replace(" ', '", ",", regex = False)
str.replace(..., regex=False) 显式关闭正则模式,做纯字面量替换,在百万级文本上更快。
由于标签在每个评论里顺序不一、数量不一,直接对整行做 value_counts() 会数不准。课程给出的巧妙解法是利用“每列即是一个槽位”的固定结构:先按逗号把字符串 split 成最多 6 个临时列 Tag_1…Tag_6(原始数据最多 6 个子标签),再用 melt 把 6 列纵向堆叠成一个大列,最后对这个大列执行 value_counts()。这在 2-notebook.ipynb 中有完整实现:
# Now split the strings into a list
tag_list_df = df.Tags.str.split(',', expand = True)
# Remove leading and trailing spaces
df["Tag_1"] = tag_list_df[0].str.strip()
df["Tag_2"] = tag_list_df[1].str.strip()
# ... Tag_3 ~ Tag_6 同理
# Merge the 6 columns into one with melt
df_tags = df.melt(value_vars=["Tag_1", "Tag_2", "Tag_3", "Tag_4", "Tag_5", "Tag_6"])
tag_vc = df_tags.value.value_counts()
该 Notebook 打印出展开后标签数据的形状为 (2514684, 2)(约 6×41.9 万行量级),并说明如何按需用 str.contains 过滤掉含 Standard|room|Stayed|device|Beds|Suite|Studio|King|Superior|Double 的标签、再以 count > 1000 收紧阈值——最终定位到值得保留的核心标签集合。统计结果显示,Tags 全表去重后共有 2428 个唯一标签,其中高频标签形如下表:
| Tag | Count |
|---|---|
| Leisure trip | 417778 |
| Submitted from a mobile device | 307640 |
| Couple | 252294 |
| Stayed 1 night | 193645 |
| Stayed 2 nights | 133937 |
| Solo traveler | 108545 |
| Stayed 3 nights | 95821 |
| Business trip | 82939 |
| Group | 65392 |
| Family with young children | 61015 |
| Stayed 4 nights | 47817 |
| Double Room | 35207 |
| Standard Double Room | 32248 |
| Superior Double Room | 31393 |
| Family with older children | 26349 |
| Deluxe Double Room | 24823 |
| Double or Twin Room | 22393 |
| Stayed 5 nights | 20845 |
| Standard Double or Twin Room | 17483 |
| Classic Double Room | 16989 |
| Superior Double or Twin Room | 13570 |
| 2 rooms | 12393 |
像 Submitted from a mobile device(来自移动设备)这类高频标签对我们毫无用处,但既然统计操作足够快,留在表里直接忽略即可,不必刻意先删。
3.3 标签取舍策略:只保留“与推荐目标相关”的两类
课程文档强调,数据集的最终目的是为“选酒店 / 做酒店推荐机器人”服务的,因此过滤标准必须锚定这个业务目标。按此逻辑对标签逐类判断:
- 旅行类型(休闲/商务)——相关,保留;
- 住客群体类型(情侣、独自旅行、家庭、团体等)——重要,保留;
- 房型 / 套房 / 工作室类型——无关(各酒店房型大同小异),剔除;
- 提交评论的设备——无关,剔除;
- 入住夜数——勉强可以说“住得久 = 更喜欢”,但证据牵强、大概率无关,剔除。
一句话概括:只保留两类标签(旅程性质 + 住客画像),其余全部放弃考虑。
剔除停留夜数标签(注意:这不是从 DataFrame 删行,而是把这些取值排除在“值得统计/入库”的标签清单之外):
| Length of stay | Count |
|---|---|
| Stayed 1 night | 193645 |
| Stayed 2 nights | 133937 |
| Stayed 3 nights | 95821 |
| Stayed 4 nights | 47817 |
| Stayed 5 nights | 20845 |
| Stayed 6 nights | 9776 |
| Stayed 7 nights | 7399 |
| Stayed 8 nights | 2502 |
| Stayed 9 nights | 1293 |
| ... | ... |
房型标签同样品种繁多却语义近似,一并从候选清单剔除:
| Type of room | Count |
|---|---|
| Double Room | 35207 |
| Standard Double Room | 32248 |
| Superior Double Room | 31393 |
| Deluxe Double Room | 24823 |
| Double or Twin Room | 22393 |
| Standard Double or Twin Room | 17483 |
| Classic Double Room | 16989 |
| Superior Double or Twin Room | 13570 |
经过这轮判断,最终幸存的“有用标签”如下(Group 与 Travelers with friends 语义几乎等价,故在统计中合并为一项):
| Tag | Count |
|---|---|
| Leisure trip | 417778 |
| Couple | 252294 |
| Solo traveler | 108545 |
| Business trip | 82939 |
| Group (combined with Travellers with friends) | 67535 |
| Family with young children | 61015 |
| Family with older children | 26349 |
| With a pet | 1405 |
3.4 生成 8 个 0/1 特征列
最后一步是把这 8 个有用标签落成新列:对每一行评论,若其 Tags 文本中包含某标签则置 1,否则置 0。最终得到“这家酒店被多少位(汇总的)评论者出于商务 / 休闲 / 携带宠物等目的选择”的统计口径,这正是酒店推荐场景所需的画像特征:
# Process the Tags into new columns
# The file Hotel_Reviews_Tags.py, identifies the most important tags
# Leisure trip, Couple, Solo traveler, Business trip, Group combined with Travelers with friends,
# Family with young children, Family with older children, With a pet
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)
注意 Group 列的两条件写法正好落地了“与 Travelers with friends 合并”的决策。值得说明的是,这 8 个“画像标签”的识别逻辑在源码中对应课程注释提到的 Hotel_Reviews_Tags.py 脚本(识别最重要标签),而标签的发现与验证过程对应 2-notebook.ipynb;1-notebook.ipynb 中则是在读取原始 CSV 后依次完成上述清洗与新列生成,并在末尾一次性删除包括 Review_Date、Review_Total_Negative_Word_Counts、Review_Total_Positive_Word_Counts、days_since_review、Total_Number_of_Reviews_Reviewer_Has_Given 在内的剩余无用列。
3.5 保存过滤结果
清洗完成后,用新文件名落盘,得到整条流水线的第一个中间产物 Hotel_Reviews_Filtered.csv(保存在数据目录,Notebook 中写入路径为 ../../data/):
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)
# Saving new data file with calculated columns
print("Saving results to Hotel_Reviews_Filtered.csv")
df.to_csv(r'../data/Hotel_Reviews_Filtered.csv', index = False)
从 1-notebook.ipynb 的实际输出看,这一整轮过滤(读入原始 CSV 起)耗时约 23.74 秒。到这里,数据质量已经大幅提升:地址粒度统一、评分口径可信、画像特征就位。
四、第二大步:用 NLTK + VADER 做情感分析
4.1 加载过滤后的数据(而不是原始数据)
情感分析阶段的核心前提是:加载的是上一节保存的 Hotel_Reviews_Filtered.csv,绝不是原始数据集。课程文档给出的骨架代码还包含后续两个阶段需要使用的所有 import:
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')
其中 nltk.download('vader_lexicon') 用于首次运行时下载 VADER 所需的词典资源(在 3-notebook.ipynb 的执行记录里可以看到它把数据安装到了本地 nltk_data 目录并返回 True)。
4.2 性能问题与停用词移除
如果直接在原始长度的正负评论两列上跑情感分析,会非常慢——课程文档给出的实测是:在配置不错的测试笔记本上,直接跑需要 12~14 分钟(具体取决于所选的情感分析库)。因此值得先做优化。
停用词(stop words) 指英语中不影响句子情感的常见词(the、and、of 之类)。把它们移除后,情感分析输入变短、运行更快,而准确率不受影响。文档给出的量化依据是:最长的一条负面评论原本 395 个词,移除停用词后只剩 195 个词,文本量接近减半。
移除停用词本身开销极低:在测试设备上对 51.5 万行的两个评论列做全量移除只需 3.3 秒(在 3-notebook.ipynb 的实测记录中为 5.77 秒——具体耗时取决于你的 CPU、内存、是否 SSD 等因素)。投入如此小、收益却可能显著,这笔优化非常划算。
代码层面有一个重要的性能细节:先把停用词表放进 set(哈希集合)再做成员判断,比逐个在原始 list 中线性查找快得多,这正是 Kaggle 上被反复验证的“快速停用词移除”做法:
from nltk.corpus import stopwords
# Load the hotel reviews from CSV
df = pd.read_csv("../../data/Hotel_Reviews_Filtered.csv")
# Remove stop words - can be slow for a lot of text!
start = time.time()
cache = set(stopwords.words("english"))
def remove_stopwords(review):
text = " ".join([word for word in review.split() if word not in cache])
return text
# Remove the stop words from both columns
df.Negative_Review = df.Negative_Review.apply(remove_stopwords)
df.Positive_Review = df.Positive_Review.apply(remove_stopwords)
review.split() 天然按空白分词,配合集合判空即得精简后的文本;Negative_Review 与 Positive_Review 两列同步处理。
4.3 用 VADER 计算情感分数
课程选用 VADER(Valence Aware Dictionary and sEntiment Reasoner) 作为情感分析器。它出自 Hutto & Gilbert(2014)在 ICWSM-14 发表的论文,是一种基于规则与情感词典、专门面向社交媒体类文本设计的模型——对短评、口语化、带表情符号与程度副词的文本尤其友好。NLTK 中其实内置了多种情感分析器可供替换比较,这里以 VADER 为主。
需要特别注意评论列的三种取值形态:数据集对“没有可写内容”的情况会用占位文本 "No Negative" / "No Positive" 填充对应列。对这类占位值直接送进分析器是没意义的,因此 calc_sentiment 先拦截它们并返回中性分 0,只有真正的评论文本才调用 polarity_scores(review)["compound"] 取综合情绪分:
from nltk.sentiment.vader import SentimentIntensityAnalyzer
# Create the vader sentiment analyser (there are others in NLTK you can try too)
vader_sentiment = SentimentIntensityAnalyzer()
# There are 3 possibilities of input for a review:
# It could be "No Negative", in which case, return 0
# It could be "No Positive", in which case, return 0
# It could be a review, in which case calculate the sentiment
def calc_sentiment(review):
if review == "No Negative" or review == "No Positive":
return 0
return vader_sentiment.polarity_scores(review)["compound"]
polarity_scores() 会返回 neg / neu / pos / compound 四个分数,其中 compound 是把三者按规则归一化到 [-1, 1] 的综合分:越接近 1 越正向,越接近 -1 越负向,0 为中性。我们只取 compound 作为一维情感特征。
随后对两个评论列逐行套用并记录耗时:
# Add a negative sentiment and positive sentiment column
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 秒”,具体到 3-notebook.ipynb 的真实执行输出则是 201.07 秒(运行机器的差异导致浮动),量级上属于“分钟级、可接受”的范围,且得益于此前停用词的移除,已经比不做预处理的 12~14 分钟大幅缩短。
4.4 用排序检查结果质量:情感分 vs 评分是否吻合
要判断打分是否合理,最直接的方法是分别按负向、正向情感分排序,肉眼抽查文本与分数的对应关系:
df = df.sort_values(by=["Negative_Sentiment"], ascending=True)
print(df[["Negative_Review", "Negative_Sentiment"]])
df = df.sort_values(by=["Positive_Sentiment"], ascending=True)
print(df[["Positive_Review", "Positive_Sentiment"]])
3-notebook.ipynb 的排序输出显示:最负向的负面评论(如 “So bad experience memories…”“First charged twice…”“No WLAN room Incredibly rude restaurant staff…”)compound 分低至 -0.99 左右,最正向的则高达 +0.99 以上;正向评论列同样呈现从 -0.98 到 +0.9987 的完整谱系,输出共覆盖 515,738 行 × 2 列。
课程同时提醒我们理性看待自动打分的错误:当情感分与评论者的星级评分矛盾时,常见解释是文本本身高度反讽,例如 “Of course I LOVED sleeping in a room with no heating”(我当然“爱死”了在没有暖气的房间睡觉)——人类一眼看出是反话,VADER 却可能判为正向。因此要预期一部分情感分数必然是错的,而且往往错得可解释;这正是后续人工校验与更复杂模型(如考虑上下文、讽刺检测)的用武之地。
4.5 重排列顺序并保存最终数据集
情感分算完后,为了让列顺序对人类友好(纯外观层面的整理),把全部新列按“酒店主键 → 评分 → 情感 → 画像 → 评论文本”的逻辑重新排列,然后落盘:
# Reorder the columns (This is cosmetic, but to make it easier to explore the data later)
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)
保存后得到整条流水线的最终产物 Hotel_Reviews_NLP.csv:它同时携带了可信的酒店级统计(自算评论数、自算均分)、两级情感分数(正/负各一个)、8 个住客画像特征以及原始评论文本——具备直接进入聚类、分类或推荐建模的资格。
五、端到端流水线回顾与课程后续
把前面所有步骤串起来,本课构成一条清晰的“四级流水线”:
- 探索:在上一课(6-NLP/4-Hotel-Reviews-1)中通过探索 Notebook 熟悉原始
Hotel_Reviews.csv的结构与分布; - 过滤:由过滤 Notebook(1-notebook.ipynb)清洗列、规范化地址、自算评分/评论数、生成画像特征,输出
Hotel_Reviews_Filtered.csv; - 情感分析:由情感 Notebook(3-notebook.ipynb)移除停用词并用 VADER 打分,输出
Hotel_Reviews_NLP.csv; - 建模:把
Hotel_Reviews_NLP.csv用于本课末尾的 NLP 挑战。
课程的“挑战”环节建议:既然每一条评论现在都有了情感分,可以尝试用本课程前面章节学过的聚类策略,围绕情感分探索隐藏模式(例如不同城市、不同旅客画像下的情感分布差异)。配套作业(assignment.md,英文原版见 6-NLP/5-Hotel-Reviews-2/assignment.md)则鼓励读者换一个全新的数据集,复刻整套“数据预处理 + NLTK 情感标注”思路,并用带说明的 Notebook 记录思考过程。
小结
回顾整条路径:起点是一份列虽多但不可尽信的原始数据集,终点则是一份“可验证 + 可检索 + 已标注情感”的高质量数据集。你经历了数据探索、按业务目标过滤无用列、把标签文本转化为结构化画像、用自算指标替换黑箱指标、借助停用词移除优化性能、最后用 VADER 给 51 万+ 条正负评论文本自动打分,也顺带认识了规则型情感分析的典型局限(讽刺、上下文缺失)。这套“清洗 → 特征化 → 情感标注”的方法论,可以原样迁移到任何文本类推荐或舆情分析任务上——这正是本课希望沉淀下来的通用能力。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00