C# 朴素贝叶斯分类器实战:从零构建垃圾邮件过滤器

1次阅读
没有评论

共计 3932 个字符,预计需要花费 10 分钟才能阅读完成。

image.webp

朴素贝叶斯算法在文本分类中表现优异,特别适合处理高维稀疏的文本数据。它的计算效率高,训练速度快,即使在小样本情况下也能保持较好的分类效果,是入门机器学习的绝佳选择。

C# 朴素贝叶斯分类器实战:从零构建垃圾邮件过滤器

文本预处理

文本预处理是机器学习中的重要环节,对于垃圾邮件过滤尤其关键。我们需要将原始文本转换成算法可以处理的数值特征。

  1. 分词处理
    对于英文文本,可以直接按空格分割单词。但中文需要特殊处理,我们可以使用开源的结巴分词等工具。
public List<string> Tokenize(string text)
{
    // 英文简单分词
    return text.ToLower()
              .Split(new[] {'',',','.','!','?'}, 
                    StringSplitOptions.RemoveEmptyEntries)
              .ToList();}
  1. 停用词处理
    去除常见但无意义的词汇,如 ”the”、”and” 等。
private HashSet<string> _stopWords = new HashSet<string> {"the", "and", "a" /*...*/};

public List<string> RemoveStopWords(List<string> tokens)
{return tokens.Where(t => !_stopWords.Contains(t)).ToList();}
  1. 词干提取
    将不同形式的单词还原为基本形式,如 ”running”→”run”。
public string Stem(string word)
{
    // 简化版词干提取
    if (word.EndsWith("ing")) return word.Substring(0, word.Length - 3);
    if (word.EndsWith("ed")) return word.Substring(0, word.Length - 2);
    return word;
}

核心算法实现

朴素贝叶斯算法的核心是计算先验概率和条件概率。

  1. 先验概率计算
    计算每个类别在训练数据中出现的概率。
private Dictionary<string, int> _classCounts = new Dictionary<string, int>();
private int _totalDocuments;

public void AddTrainingSample(string text, string category)
{if (!_classCounts.ContainsKey(category))
        _classCounts[category] = 0;

    _classCounts[category]++;
    _totalDocuments++;

    // 更新词频统计...
}

public double GetPriorProbability(string category)
{return (double)_classCounts[category] / _totalDocuments;
}
  1. 条件概率计算
    计算每个词在每个类别中出现的概率。
private Dictionary<string, Dictionary<string, int>> _wordCategoryCounts = 
    new Dictionary<string, Dictionary<string, int>>();

private Dictionary<string, int> _categoryWordTotals = new Dictionary<string, int>();

private double GetWordProbability(string word, string category)
{
    // 拉普拉斯平滑
    int count = _wordCategoryCounts.GetValueOrDefault(category, new Dictionary<string, int>())
                 .GetValueOrDefault(word, 0);
    int total = _categoryWordTotals.GetValueOrDefault(category, 0);

    return (double)(count + 1) / (total + _vocabularySize);
}
  1. 完整分类器实现
public class NaiveBayesClassifier
{
    // 类成员变量...

    public void AddTrainingSample(string text, string category)
    {
        // 更新类别计数
        if (!_classCounts.ContainsKey(category))
            _classCounts[category] = 0;
        _classCounts[category]++;
        _totalDocuments++;

        // 更新词频统计
        var tokens = PreprocessText(text);
        if (!_wordCategoryCounts.ContainsKey(category))
            _wordCategoryCounts[category] = new Dictionary<string, int>();

        if (!_categoryWordTotals.ContainsKey(category))
            _categoryWordTotals[category] = 0;

        foreach (var token in tokens)
        {if (!_wordCategoryCounts[category].ContainsKey(token))
                _wordCategoryCounts[category][token] = 0;

            _wordCategoryCounts[category][token]++;
            _categoryWordTotals[category]++;

            // 更新词汇表大小
            if (!_vocabulary.Contains(token))
            {_vocabulary.Add(token);
                _vocabularySize++;
            }
        }
    }

    public string Classify(string text)
    {var tokens = PreprocessText(text);
        var categories = _classCounts.Keys;

        string bestCategory = null;
        double maxScore = double.MinValue;

        foreach (var category in categories)
        {double score = Math.Log(GetPriorProbability(category));

            foreach (var token in tokens)
            {score += Math.Log(GetWordProbability(token, category));
            }

            if (score > maxScore)
            {
                maxScore = score;
                bestCategory = category;
            }
        }

        return bestCategory;
    }
}

性能优化

  1. 内存优化
  2. 使用高效的哈希函数减少碰撞
  3. 对于大型数据集,考虑使用稀疏矩阵存储

  4. 多线程训练

  5. 将训练数据分片处理
  6. 使用 ConcurrentDictionary 保证线程安全
  7. 注意锁的粒度,避免性能下降

端到端示例

// 加载数据集
var dataset = File.ReadAllLines("SMSSpamCollection.txt")
                 .Select(line => line.Split(new[] {'\t'}, 2))
                 .Select(parts => new { Label = parts[0], Text = parts[1] })
                 .ToList();

// 创建并训练分类器
var classifier = new NaiveBayesClassifier();
foreach (var item in dataset)
{classifier.AddTrainingSample(item.Text, item.Label);
}

// 测试分类器
var testMessage = "WINNER!! You've been selected to receive a free vacation!";
var result = classifier.Classify(testMessage);
Console.WriteLine($"Message: {testMessage}");
Console.WriteLine($"Classification: {result}");

生产环境建议

  1. 特征选择
  2. 使用卡方检验或信息增益选择最有区分度的特征
  3. 考虑 n -gram 特征提取

  4. 处理数值下溢

  5. 使用对数概率避免连乘导致的数值下溢
  6. 实现示例中已经采用了这一策略

  7. 模型持久化

  8. 使用 Protobuf 进行序列化
  9. 定期保存模型快照
public void SaveModel(string path)
{using (var file = File.Create(path))
    {Serializer.Serialize(file, this);
    }
}

public static NaiveBayesClassifier LoadModel(string path)
{using (var file = File.OpenRead(path))
    {return Serializer.Deserialize<NaiveBayesClassifier>(file);
    }
}

思考题

  1. 如何改进算法处理一词多义问题?可以考虑引入词向量 (word2vec) 或上下文信息。
  2. 当新类别出现时如何增量训练?需要设计机制来动态扩展词汇表和类别集合。

通过这个实战项目,我们不仅实现了一个可用的垃圾邮件过滤器,更重要的是掌握了朴素贝叶斯算法的核心思想和实现技巧。虽然这是一个入门级项目,但它包含了机器学习系统开发的完整流程,为后续更复杂的项目打下了坚实基础。

正文完
 0
评论(没有评论)