一、自然语言处理与C语言:一场意外的邂逅
说实话,很多人听到「自然语言处理」这个词,第一反应就是Python、NLTK、spaCy这些库。我当年也是这么想的。直到有一次,我在嵌入式设备上做文本分析——那台设备的内存只有256KB,跑Python?想都别想。
这时候我才意识到,C语言在NLP领域其实有它独特的价值。你想想看,很多底层文本处理库,比如ICU、libunistring,底层都是C写的。说白了,C语言做NLP,就像用螺丝刀修手表——工具看着笨重,但精度极高。
今天我们就从最基础的四个方向入手:分词、关键词提取、命名实体识别、情感分析。我会用C语言把这些概念落地,让你看到底层的实现逻辑。
核心观点:C语言做NLP不是为了替代Python,而是让你理解「文本处理」的本质——字符串操作、统计计算、模式匹配。掌握了这些,你用任何语言都能写出高效的NLP程序。
二、分词:把句子拆成最小单元
2.1 为什么分词是第一步?
分词,说白了就是把一句话切成有意义的片段。比如「我喜欢编程」要切成「我」「喜欢」「编程」。英文分词简单,按空格切就行。中文?嗯,这里要注意——中文没有天然的分隔符。
我在项目中遇到过最头疼的问题:用户输入「武汉市长江大桥」,到底是「武汉/市长/江大桥」还是「武汉市/长江大桥」?这就是歧义分词的典型例子。
2.2 基于字典的最大匹配法
最简单的分词方法就是「最大匹配法」。我习惯用正向最大匹配,也就是从左到右,每次取最长的匹配词。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 模拟一个简单的词典
const char* dict[] = {"我", "喜欢", "编程", "自然", "语言", "处理", "武汉", "长江", "大桥", NULL};
// 正向最大匹配分词
void forward_max_match(const char* text) {
int len = strlen(text);
int max_word_len = 4; // 假设最长词为4个字符
int i = 0;
while (i < len) {
int matched = 0;
// 从最长长度开始尝试匹配
for (int j = max_word_len; j > 0; j--) {
if (i + j > len) continue;
char sub[16] = {0};
strncpy(sub, text + i, j);
// 在词典中查找
for (int k = 0; dict[k] != NULL; k++) {
if (strcmp(sub, dict[k]) == 0) {
printf("[%s] ", sub);
i += j;
matched = 1;
break;
}
}
if (matched) break;
}
// 没匹配到,按单字处理
if (!matched) {
printf("[%c] ", text[i]);
i++;
}
}
printf("\n");
}
int main() {
char text[] = "我喜欢自然语言处理";
printf("原文: %s\n", text);
printf("分词结果: ");
forward_max_match(text);
return 0;
}
我的经验:最大匹配法虽然简单,但实际项目中我一般会配合「逆向最大匹配」一起用。正向和逆向结果不一致时,再通过统计方法选择更合理的那个。这叫「双向匹配法」,准确率能提升10%左右。
三、关键词提取:找出句子的灵魂
3.1 TF-IDF:最经典的关键词算法
关键词提取,说白了就是找出「最能代表这段文本」的词语。TF-IDF(词频-逆文档频率)是我用得最多的方法。它的核心思想很简单:
- TF(词频):这个词在本文中出现了多少次
- IDF(逆文档频率):这个词在所有文档中出现的频率有多低
为什么要有IDF?你想想看,「的」「了」「是」这些词几乎每篇文章都有,它们能代表文章主题吗?显然不能。IDF就是用来惩罚这些常见词的。
3.2 C语言实现TF-IDF
#include <stdio.h>
#include <string.h>
#include <math.h>
#define MAX_DOCS 100
#define MAX_WORDS 1000
typedef struct {
char word[32];
int doc_count; // 包含该词的文档数
double idf; // 逆文档频率
} WordStat;
// 计算IDF
void compute_idf(WordStat* stats, int word_count, int total_docs) {
for (int i = 0; i < word_count; i++) {
if (stats[i].doc_count > 0) {
stats[i].idf = log((double)total_docs / stats[i].doc_count);
}
}
}
// 计算TF-IDF得分
double compute_tfidf(int tf, double idf) {
return (1 + log(tf)) * idf; // 使用平滑后的TF
}
int main() {
// 模拟3篇文档
const char* docs[] = {
"我喜欢编程和自然语言处理",
"自然语言处理是人工智能的重要分支",
"编程需要不断练习和实践"
};
int doc_count = 3;
// 这里简化处理,实际需要完整的分词和统计
WordStat stats[] = {
{"自然", 2, 0}, // 出现在文档0和1中
{"语言", 2, 0},
{"处理", 2, 0},
{"编程", 2, 0}, // 出现在文档0和2中
{"人工智能", 1, 0}
};
int word_count = 5;
compute_idf(stats, word_count, doc_count);
printf("关键词IDF值:\n");
for (int i = 0; i < word_count; i++) {
printf(" %s: IDF=%.3f\n", stats[i].word, stats[i].idf);
}
// 计算文档0中每个词的TF-IDF
printf("\n文档0的关键词得分:\n");
int tf_values[] = {1, 1, 1, 1, 0}; // 文档0中各词出现次数
for (int i = 0; i < word_count; i++) {
if (tf_values[i] > 0) {
double score = compute_tfidf(tf_values[i], stats[i].idf);
printf(" %s: TF-IDF=%.3f\n", stats[i].word, score);
}
}
return 0;
}
避坑指南:我曾经在计算IDF时忘记处理「分母为零」的情况——如果某个词在所有文档中都出现,IDF会变成0。更极端的是,如果某个词从未出现过,log(0)会直接崩溃。所以一定要加平滑处理,比如 IDF = log((N+1)/(df+1)) + 1。
四、命名实体识别:找到人名、地名、机构名
4.1 基于规则的方法
命名实体识别(NER),说白了就是从文本中找出「专有名词」。比如「张三去了北京」——「张三」是人名,「北京」是地名。
我个人的习惯是:先用规则匹配,再用统计模型兜底。规则匹配虽然简单,但在特定领域效果出奇的好。比如在医疗领域,「阿司匹林」「布洛芬」这些药名,用字典匹配准确率能到95%以上。
4.2 简单的规则引擎实现
#include <stdio.h>
#include <string.h>
#include <ctype.h>
typedef enum {
ENTITY_PERSON,
ENTITY_LOCATION,
ENTITY_ORGANIZATION,
ENTITY_UNKNOWN
} EntityType;
typedef struct {
char name[64];
EntityType type;
} Entity;
// 简单的实体词典
Entity entity_dict[] = {
{"张三", ENTITY_PERSON},
{"李四", ENTITY_PERSON},
{"北京", ENTITY_LOCATION},
{"上海", ENTITY_LOCATION},
{"华为", ENTITY_ORGANIZATION},
{"阿里巴巴", ENTITY_ORGANIZATION},
{"", ENTITY_UNKNOWN} // 结束标记
};
// 识别命名实体
void recognize_entities(const char* text) {
printf("原文: %s\n", text);
printf("识别到的实体:\n");
int len = strlen(text);
int i = 0;
while (i < len) {
int found = 0;
// 尝试匹配词典中的实体
for (int j = 0; entity_dict[j].type != ENTITY_UNKNOWN; j++) {
int ent_len = strlen(entity_dict[j].name);
if (i + ent_len <= len) {
if (strncmp(text + i, entity_dict[j].name, ent_len) == 0) {
const char* type_str;
switch (entity_dict[j].type) {
case ENTITY_PERSON: type_str = "人名"; break;
case ENTITY_LOCATION: type_str = "地名"; break;
case ENTITY_ORGANIZATION: type_str = "机构名"; break;
default: type_str = "未知"; break;
}
printf(" [%s] -> %s\n", entity_dict[j].name, type_str);
i += ent_len;
found = 1;
break;
}
}
}
if (!found) {
i++; // 跳过未识别的字符
}
}
}
int main() {
recognize_entities("张三去了北京,在华为工作");
return 0;
}
进阶技巧:实际项目中,我还会加入「上下文规则」。比如「XX教授」「XX市长」后面的词大概率是人名;「位于XX」「去XX」后面的词大概率是地名。这些规则虽然简单,但能大幅提升召回率。
五、情感分析:判断文字是褒是贬
5.1 基于情感词典的方法
情感分析,说白了就是判断一段话是正面还是负面。最简单的方法就是「情感词典法」——把句子中的褒义词和贬义词分别计数,哪个多就偏向哪边。
我记得有一次给电商平台做评论分析,用户说「这个手机电池续航不错,但屏幕太差了」。如果只看情感词,「不错」是正面,「太差」是负面,两个词抵消了。但实际用户是「褒贬各半」,这时候就需要更精细的权重计算。
5.2 简单的情感分析器
#include <stdio.h>
#include <string.h>
// 情感词典(简化版)
const char* positive_words[] = {"好", "棒", "喜欢", "不错", "优秀", "满意", NULL};
const char* negative_words[] = {"差", "坏", "讨厌", "糟糕", "垃圾", "失望", NULL};
// 计算情感得分
double sentiment_analysis(const char* text) {
int pos_count = 0;
int neg_count = 0;
// 统计正面词
for (int i = 0; positive_words[i] != NULL; i++) {
if (strstr(text, positive_words[i]) != NULL) {
pos_count++;
}
}
// 统计负面词
for (int i = 0; negative_words[i] != NULL; i++) {
if (strstr(text, negative_words[i]) != NULL) {
neg_count++;
}
}
// 计算得分:范围 -1 到 1
int total = pos_count + neg_count;
if (total == 0) return 0.0;
return (double)(pos_count - neg_count) / total;
}
int main() {
const char* texts[] = {
"这个产品非常好,我很喜欢",
"质量太差了,真糟糕",
"今天天气不错",
"这个手机屏幕好,但电池差"
};
for (int i = 0; i < 4; i++) {
double score = sentiment_analysis(texts[i]);
printf("文本: %s\n", texts[i]);
printf("情感得分: %.2f (", score);
if (score > 0.3) printf("正面");
else if (score < -0.3) printf("负面");
else printf("中性");
printf(")\n\n");
}
return 0;
}
实际项目经验:情感词典法最大的问题是「否定词」。比如「不好」——「好」是正面词,但前面加了个「不」,意思完全反了。我后来加了一个规则:如果正面词前面3个字符内有「不」「没」「无」等否定词,就把情感值取反。这个小改动让准确率从65%提升到了78%。
六、知识体系总览
下面这张图总结了本章的核心内容。我习惯用流程图来梳理知识结构,这样一眼就能看清各个模块之间的关系。
七、本章小结
今天我们走完了NLP入门的四个基础步骤:
- 分词:用最大匹配法把句子切成词
- 关键词提取:用TF-IDF找出重要词汇
- 命名实体识别:用规则匹配找到人名、地名、机构名
- 情感分析:用情感词典判断文字倾向
说实话,这些方法在工业界已经用了十几年了。虽然现在深度学习很火,但底层的基础方法依然有它的价值——尤其是在资源受限的环境下,或者你需要快速验证一个想法的时候。
我个人建议:先把这些基础方法吃透,再去学复杂的模型。因为你会发现,无论多高级的NLP系统,底层都离不开这些基本操作。
课后练习:试着用今天学到的知识,写一个简单的「文本分类器」。比如判断一段新闻是属于「体育」「科技」还是「娱乐」类别。提示:可以用关键词匹配 + 类别词典的方式实现。
公众号:蓝海资料掘金营,微信deep3321