在当今的数字世界中,垃圾邮件早已不再只是令人烦恼的东西——它正逐渐成为一种严重的安全威胁。为了应对这一挑战,开发者们常常会借助机器学习技术来构建智能过滤器,以便区分合法邮件和恶意邮件。

虽然在使用笔记本环境构建机器学习模型相对较为简单,但真正的难点在于将这个模型部署到一个可扩展、能够投入实际使用的系统中。

在这个项目中,我开发了一个端到端的无服务器垃圾邮件分类系统。该系统结合了Scikit-learn进行模型开发,同时利用AWS Lambda、Amazon S3和Amazon API Gateway来完成模型的部署工作。最终得到的结果是一个轻量级、可扩展的API,它能够实时地对邮件内容进行分类。

这个系统的设计理念是模块化且成本效益高,因此用户可以独立地对模型进行重新训练或更新,而不会影响到正在运行的API服务。无论是检测“免费iPhone”骗局,还是识别网络钓鱼攻击,这个项目都展示了如何将机器学习实验成果转化为实际应用。

目录

1. 先决条件

  1. 基本要求:具备Python编程基础,并了解机器学习中的分类等相关概念。

  2. AWS账户:需要拥有一个具有Lambda、S3和API Gateway使用权限的AWS账户。

  3. 开发环境:需安装Python 3.11,同时还需要scikit-learn、pandas和joblib等相关库。

  4. AWS CLI:需要在本地机器上配置好AWS CLI工具,以便进行文件上传操作。

  5. HuggingFace账户:你可以直接从我的账户下载所需的模型文件。

2. 构建模型

用于展示AI模型的示意图

图片由 Steve A JohnsonUnsplash 上提供。

这个项目的核心是一种监督学习方法。我们不会简单地指定哪些词属于垃圾邮件,而是为计算机提供一组数据集和相应的算法,让它能够自行学习和识别垃圾邮件的特征。

1. 向量化:将文本转化为数学表达式

机器学习模型无法阅读文本,它们需要数值形式的输入。为了解决这个问题,我们使用了TF-IDF向量化工具。

feature_extraction = TfidfVectorizer(min_df=1, stop_words='english', lowercase=True)
X_train_features = feature_extraction.fit_transform(X_train)

其数学公式如下:

$$w_{i,j} = tf_{i,j} \times \log \left( \frac{N}{df_i} \right)$$

TF-IDF的相关术语定义如下:

  • wᵢ,ⱼ(权重):某个特定单词在文档中的最终重要性得分。

  • tfᵢ,ⱼ(词频):一个单词在某封邮件中出现的次数。

  • N(总文档数):数据集中所有邮件的总数。

  • dfᵢ(文档频率):包含某个特定单词的不同邮件的数量。

  • log(N/dfᵢ)(IDF值):用于降低那些在所有邮件中都出现的常见单词的得分。

该方法会通过去除常见词汇、将所有文本转换为小写来保持数据的一致性,同时赋予罕见且具有意义的词汇更高的重要性,而降低常用词汇的重要性。

2. 训练:逻辑回归模型的训练过程

在这里,我们将使用逻辑回归算法,这种分类算法可以预测某种结果出现的概率。

在这一阶段,我们会将经过向量化处理的训练数据输入到逻辑回归模型中。我们的目标是建立特定单词的权重与“垃圾邮件”或“合法邮件”标签之间的数学关系。

在训练过程中,模型会不断调整自身的内部参数以最小化误差,最终它会学会识别出像“winner”或“free”这样的词与垃圾邮件有很高的关联度,而像“conversational language”这样的词则与合法邮件相关。

model = LogisticRegression()
model.fit(X_train_features, Y_train)

在我们的案例中,该模型会计算出一封邮件属于垃圾邮件还是合法邮件的概率。

该算法使用Sigmoid函数将任何实数映射到0到1之间的值。

$$P(y=1|x) = \frac{1}{1 + e^{-(z)}}$$

其中z = β₀ + β₁x₁ + … + βₙxₙ。

3. 评估:测试模型的智能水平

在完成训练之后,我们需要验证该模型是否真的能够处理那些它之前从未见过的数据。

prediction_on_test_data = model.predict(X_test_features)
accuracy_on_test_data = accuracy_score(Y_test, prediction_on_test_data)

通过将模型的预测结果与我们测试集中的实际标签进行比较,我们可以计算出准确率。这一数据让我们确信该模型已经具备了在实际环境中运行的能力(在我们的测试中,其准确率达到了约94%)。

4. 逻辑代码的导出与序列化

为了将这个模型从我们的本地Python环境转移到AWS云平台上,我们将使用Joblib工具将我们的代码保存为二进制文件(.pkl格式)。

joblib.dump(model, 'spam_model.pkl')
joblib.dump(feature_extraction, 'vectorizer.pkl')

我们选择使用Pickle格式,是因为它能够将复杂的Python对象(比如数学权重和词汇映射关系)转换为一种便携的二进制格式,这样这些数据就可以在云环境中被立即重新使用。

我们需要Vectorizer工具来将新用户输入的文本转换成模型所能识别的具体数值坐标。如果没有Vectorizer,就相当于只有钥匙而没有锁一样,根本无法使用这个模型。

我们已经训练好的逻辑回归模型以及TF-IDF向量化工具,可以在Hugging Face网站上供大家免费使用:在Hugging Face上获取这些模型

3. 将模型部署到AWS上

训练模型属于科学范畴,而将其部署到实际环境中则属于工程技术。为了让全世界的人都能使用这个分类器,我们将采用一种无需维护、能够自动扩展的无服务器架构。

1. 模型存储:Amazon S3

首先,我们会将那些.pkl文件上传到Amazon S3桶中。由于模型代码与业务逻辑是分离的,因此我们只需更新S3中的文件即可更新模型的功能,而无需重新部署后端代码。这样的设计使得系统具有很高的可维护性。

2. 生产环境:AWS Lambda

为了让人工智能服务能够被广泛使用,我们会从本地脚本转向无服务器云架构。这种架构可以确保模型随时都能正常运行,而且无需花费额外的成本来维持24/7运行的服务器。

我们的部署环境是AWS Lambda(使用Python 3.11语言)。由于Lambda是一个轻量级的平台,因此它并不包含Scikit-Learn或Joblib这些工具。为了使用这些库,我们会将它们下载下来并存储在S3桶中,然后通过模型结构导入到相应的位置。

AWS CLI中的相关命令:


# 1. 创建工作目录
mkdir ml_layer & cd ml_layer

# 2. 将scikit-learn及其依赖库安装到指定文件夹中
pip install \
--platform manylinux2014_x86_64 \
--target=python/lib/python3.11/site-packages \
--implementation cp \
--python-version 3.11 \
--only-binary=:all: \
scikit-learn joblib

# 3. 将安装好的库压缩成.zip文件
zip -r sklearn_lib.zip python

# 4. 使用AWS CLI将压缩文件上传到S3桶
aws s3 cp sklearn_lib.zip s3://YOUR-Bucket-NAME/

我们将Scikit-Learn库存储在S3中,并将其压缩成ZIP文件,这样就可以绕过AWS Lambda对部署包大小的限制。这样一来,该函数只有在需要时才会动态加载那些占用大量资源的依赖库,从而避免使核心代码变得过于庞大。

Lambda函数:


import json
import boto3
import os
import sys
from io import BytesIO

# 确保能够找到包含scikit-learn/joblib的定制Lambda层
sys.path.append('/opt/python')

try:
    import joblib
except ImportError:
    # 为特定的Scikit-Learn版本提供备用解决方案
    from sklearn.utils import _joblib as joblib

# 初始化S3客户端
s3 = boto3.client('s3')

# 使用占位符来表示文章内容,以便读者可以自行填写实际数据
BUCKET_NAME = 'YOUR_S3_BUCKET_NAME' 
MODEL_KEY = 'spam_model.pkl'
VECTORIZER_KEY = 'vectorizer.pkl'

# 用于“热启动”缓存机制的全局变量(这种机制可以通过将模型保留在内存中来提升性能)
model = None
vectorizer = None

def load_model():
    """仅当模型文件尚未存在于内存中时,才会从S3下载这些文件"""
    global model, vectorizer
    if model is None or vectorizer is None:
        try:
            # 1. 从S3下载逻辑回归模型文件
            m_obj = s3.get_object(Bucket=BUCKET_NAME, Key=MODEL_KEY)
            model = joblib.load(BytesIO(m_obj['Body'].read()))

            # 2. 直接从S3下载TF-IDF向量器文件
            v_obj = s3.get_object(Bucket=BUCKET_NAME, Key=VECTORIZER_KEY)
            vectorizer = joblib.load(bytesIO(v_obj['Body'].read()))
        except Exception as e:
            raise Exception(f"无法从S3下载.pkl文件:{str(e)}")

def lambda_handler(event, context):
    try:
        # 在开始处理之前,确保模型和向量器已经准备好
        load_model()

        # 该函数既能够处理直接的Lambda测试请求,也能够处理API Gateway发送的POST请求
        body = event.get('body', event)
        if isinstance(body, str):
            body = json.loads(body)

        text = body.get('text', '')
        
        if not text:
            return {
                'statusCode': 400,
                'body': json.dumps({'error': '未提供文本内容。'}
              }

        # 1. 使用训练好的向量器将输入文本转换为数值特征
        data_vec = vectorizer.transform([text])

        # 2. 使用逻辑回归模型进行预测
        prediction = int(model.predict(data_vec)[0])

        # 3. 将预测结果转换为人类可识别的标签
        result_label = "HAM" if prediction == 1 else "SPAM"

        # 返回响应信息,并设置CORS头
        return {
            'statusCode': 200,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*' # 这个头部是用于实现跨域Web集成的
            },
            'body': json.dumps({
                'status': 'success',
                'classification': result_label,
                'input_text': text
            })
        }
        
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps({'error_message': f"推理过程中出现错误:{str(e)}')
        }

Lambda函数的主要特性:

  1. 预热缓存机制:通过在lambda_handler之外定义模型和向量化器变量,我们可以将这些数据存储在容器的内存中。这样一来,后续请求的启动延迟就会显著降低。

  2. 动态依赖项加载:通过执行sys.path.append(‘/opt/python’)这条指令,我们能够从S3或Layers服务器导入大型库文件,而不会超过上传限制。

  3. 双模式输入处理:该函数被设计为既能接收来自AWS控制台的直接JSON格式数据,也能处理通过API Gateway发送的字符串化请求体。

3. API Gateway——连接互联网的桥梁

用于演示API Gateway功能的图片。

图片由GrowtikaUnsplash平台上提供。

创建REST API

接下来,我们将创建一个仅包含POST方法的REST API。为什么选择POST方法呢?因为我们需要将包含用户文本信息的JSON数据安全地发送给我们的模型。

  1. 首先登录Amazon API Gateway控制台,然后选择“创建API”→“REST API”。

  2. 为你的API起一个名称,比如“EmailSpamPredictor-API”,并将端点类型设置为“区域级”。

  3. 在左侧侧边栏中点击“资源”,然后输入一个资源名称(例如我输入的/predict)。

  4. 接下来点击“创建方法”,选择POST,再将集成类型设置为Lambda Function。

  5. 确保已启用Lambda Proxy集成功能——这样整个请求流程才能顺利传递到你的代码中。

CORS配置(故障排查的关键环节)
许多开发者都会在这里遇到令人头疼的连接错误问题。由于我们的API托管在AWS上,而如果前端应用位于另一个网站上,浏览器的同源策略会默认阻止这种请求。

为了解决这个问题,我们需要启用CORS配置:

  1. Access-Control-Allow-Origin:将其设置为*(或具体指定你的域名),这样浏览器就会知道该API可以被前端应用访问。

  2. OPTIONS方法:API Gateway会自动生成OPTIONS请求。这个请求用于验证:在发送实际数据之前,浏览器会先询问“是否允许从我的端点接收数据?”

  3. Access-Control-Allow-Headers:如截图所示,Content-Type和Authorization等头部信息是被允许的。这样,当我们的JavaScript代码使用fetch()方法并设置内容类型为application/json时,API Gateway就不会拒绝这些请求。

该图片展示了我们项目的CORS配置情况。

该图片展示了我们项目的CORS配置情况。(图片由作者提供)

部署阶段

一旦API被部署到生产环境,AWS会生成一个永久性的调用URL。这个URL充当我们模型的公共访问接口,其格式通常如下:https://[api-id].execute-api.[region].amazonaws.com/prod/classify

连接前端与JavaScript层

现在API已经可以正常使用了,我们可以编写一个简单的JavaScript函数来与我们的模型进行交互。每当用户点击网站上的分析按钮时,这个脚本就会被执行。


async function checkSpam() {
    const message = document.getElementById("userInput").value;
    const apiUrl = "YOUR_API_GATEWAY_INVOKE_URL";

    try {
        const response = await fetch(apiUrl, {
            method: "POST",
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify({ "text": message })
        });

        const data = await response.json();
        
        // 在网页上显示结果
        const resultElement = document.getElementById("result");
        resultElement.innerText = `预测结果:${data.classification}`;
        resultElement.style.color = data.classification === "SPAM" ? "red" : "green";

    } catch (error) {
        console.error("错误发生:", error);
        alert("无法连接到Spam检测API。");
    }
}

4. 如何在本地运行该项目

你可以将前端代码保存为HTML文件。准备就绪后,不要直接双击该.html文件——在浏览器中将其作为普通文件打开可能会引发安全问题。相反,你应该使用一个简单的本地服务器来托管它。

步骤1: 打开终端或命令提示符。

步骤2: 导航到你的项目文件夹。

cd [PATH_TO_YOUR_FOLDER]

步骤3: 启动一个本地的Python Web服务器。

python -m http.server 8000

步骤4: 访问应用程序。

打开浏览器,然后访问以下地址:
http://localhost:8000/your-file-name.html

观看演示:

5. 我们的项目架构

展示我们项目架构图的图片。

这张图片展示了我们项目的架构(即构建一个无服务器垃圾邮件分类系统)。它展示了从用户输入到最终模型输出这一整个处理流程。(图片由作者提供)

  1. 客户端前端交互:整个过程最左侧开始。用户通过网页界面或桌面应用程序进行操作,他们输入诸如“立即赢取免费iPhone”这样的文本,从而触发请求。

  2. 入口点:API网关:请求首先到达Amazon API网关,该网关充当“安全守卫”和数据转换器的作用。
    (a) CORS OPTIONS机制用于处理预请求握手过程,以确保浏览器获得与AWS云平台进行交互的权限。
    (b)分类请求(POST请求)会将实际的消息数据转发到后端逻辑处理模块。

  3. 核心处理引擎:AWS Lambda(Python 3.11语言):位于中间的这个组件代表Lambda函数。你编写的代码就存储在这里。Lambda函数并不会全天候运行,只有当有请求到达时才会被激活。

  4. 数据存储与检索:S3存储桶:由于Lambda函数的体积较小,因此它不会将庞大的机器学习模型文件保存在自身内部。
    依赖库及模型的下载:Lambda函数会从S3存储桶中下载sklearn库以及.pkl格式的模型文件。
    必需的依赖资源:这些文件会被加载到Lambda函数的临时内存中,以便进行后续的预测计算。

  5. 推理流程:在Lambda函数内部,会执行三个步骤的数学处理:
    (a) 文本向量化:将文本中的单词转换为数字形式。
    (b) 逻辑回归:根据这些数字计算出垃圾邮件的概率。
    (c> 分类结果生成:最终确定邮件属于“垃圾邮件”还是“正常邮件”。

  6. 结果输出:处理结果会通过API网关返回给客户端,同时附带必要的CORS头部信息,以确保浏览器能够正确接收这些数据。前端界面随后会更新显示“结果:垃圾邮件”这一结论,并附有相应的视觉提示。

6. 结论:无服务器AI的强大能力

通过将逻辑回归的数学简洁性与AWS无服务器架构的强大功能相结合,我们成功地将一个普通的Python脚本转化成了一个可全球使用的、具备扩展性的API服务。

这个项目证明了:部署高质量的机器学习系统并不需要巨额预算,也不必使用全天候运行的专用服务器。

通过使用S3与Lambda相结合的解决方案,我们成功绕过了常见的存储障碍,从而确保我们的“大脑”(模型)及其“肌肉”(Scikit-Learn框架)能够在云环境的临时性环境中顺畅运行。这一方案有效地弥合了实验研究与实际应用之间的差距,使人工智能系统变得实用、高效且易于使用。

7. 致谢 / 参考文献

与我联系

您可能还会喜欢:

  1. Polars是如何超越Pandas的?

  2. DevOps已死,平台工程万岁!

Comments are closed.