2024年10月

在现代软件开发中,性能和用户体验是至关重要的,而负载测试和自动化测试可以帮助我们实现这一目标。在本文中,我们将讨论如何在 Kubernetes 环境中运行 Locust 和 Selenium,并详细介绍如何安装 Chrome 和 ChromeDriver。

1. Dockerfile 配置

首先,我们需要创建一个 Dockerfile,以构建一个包含 Locust 和 Selenium 的 Docker 镜像。以下是 Dockerfile 的内容:

FROM locustio/locust:2.31.3# 设置 Chrome 的版本ENV CHROME_VERSION 130.0.6723.69

USERrootRUNapt-get update -qq -y && \
apt-get install -y wget unzip && \
apt-get install -y \
libasound2 \
libatk-bridge2.
0-0\
libgtk-
4-1\
libnss3 \
xdg-utils && \
wget -q -O chrome-linux64.zip https:
//storage.googleapis.com/chrome-for-testing-public/$CHROME_VERSION/linux64/chrome-linux64.zip && \ unzip chrome-linux64.zip && \
rm chrome-linux64.zip && \
mv chrome-linux64 /opt/chrome/ && \
ln -s /opt/chrome/chrome /usr/local/bin/ && \
wget -q -O chromedriver-linux64.zip https:
//storage.googleapis.com/chrome-for-testing-public/$CHROME_VERSION/linux64/chromedriver-linux64.zip && \ unzip -j chromedriver-linux64.zip chromedriver-linux64/chromedriver && \
rm chromedriver-linux64.zip && \
mv chromedriver /usr/local/bin/

# 设置 Chrome 的配置和缓存目录
ENVXDG_CONFIG_HOME=/tmp/.chromiumENVXDG_CACHE_HOME=/tmp/.chromiumCOPY. .RUNpip install -r requirements.txt

解释

  1. 基础镜像
    :使用
    locustio/locust
    作为基础镜像。
  2. 安装依赖
    :更新包管理器并安装必要的库,以确保 Chrome 和 ChromeDriver 正常运行。
  3. 下载和安装 Chrome 和 ChromeDriver
    :从
    Google 的存储库
    下载 Chrome 和 ChromeDriver。
  4. 配置目录
    :通过环境变量设置 Chrome 的配置和缓存目录,这一步非常重要。若未设置正确,可能会在 Kubernetes 中出现权限问题,具体参考
    puppeteer-sharp

requirements.txt 示例

locust=2.31.3selenium==4.21.0

2. Chrome 选项配置

在使用 Selenium 时,我们需要为 Chrome 配置一些选项,以确保它能够在无头模式下正常工作。以下是获取 Chrome 选项的代码示例:

import platform
from selenium import webdriver

def is_running_in_linux():
return platform.system() ==
'Linux'def get_chrome_options():
is_in_linux = is_running_in_linux()
options_custom = webdriver.ChromeOptions()

# Linux 下的 Chrome 选项
if is_in_linux:
options_custom.add_argument(
"--headless") # 无头模式
options_custom.add_argument(
'--disable-gpu') # 禁用 GPU 加速
options_custom.add_argument(
"--no-sandbox") # 禁用沙箱模式
else:
options_custom.add_argument(
"--start-maximized") # 启动时最大化窗口

# 其他通用选项
options_custom.add_argument(
"--disable-dev-shm-usage") # 解决资源限制问题
options_custom.add_argument(
"--ignore-ssl-errors=yes") # 忽略 SSL 错误
options_custom.add_argument(
"--disable-cache") # 禁用缓存

return options_custom

解释

  • 操作系统检测
    :根据当前操作系统选择适当的 Chrome 选项。
  • 无头模式
    :在 Linux 环境中使用无头模式,以便在没有图形界面的情况下运行 Chrome。
  • 禁用沙箱
    :在 Kubernetes 环境中,禁用沙箱模式可以避免潜在的权限问题。

3. Locust 用户定义

下面是一个简单的 Locust 用户示例,使用 Selenium 控制 Chrome 访问特定页面:

from locust import User, task

class GetUrl(User):
customdriver = None

def on_start(self):
self.customdriver = webdriver.Chrome(options=get_chrome_options())

@task
def load_page(self):
self.customdriver.get(
"http://example.com") # 根据需要替换为实际 URL

解释

  • 用户定义
    :创建一个继承自
    User
    的类,使用 Selenium 控制 Chrome。
  • 启动时操作
    :在用户启动时初始化
    customdriver
  • 任务定义
    :在
    load_page
    方法中执行实际的页面加载操作。

4. Kubernetes 部署

完成 Dockerfile 和代码后,可以将其构建为 Docker 镜像,并在 Kubernetes 中部署。以下是一个基本的 Kubernetes YAML 配置示例:

apiVersion: apps/v1
kind: Deployment
metadata:
name: locust
spec:
replicas:
1selector:
matchLabels:
app: locust
template:
metadata:
labels:
app: locust
spec:
containers:
- name: locust
image: your-docker-image:latest
ports:
- containerPort:
8089env:
- name: XDG_CONFIG_HOME
value: /tmp/.chromium # 设置 Chrome 的配置目录
- name: XDG_CACHE_HOME
value: /tmp/.chromium # 设置 Chrome 的缓存目录
---
apiVersion: v1
kind: Service
metadata:
name: locust-service
spec:
type: NodePort
ports:
- port:
8089targetPort:8089selector:
app: locust

解释

  • Deployment
    :定义 Locust 的 Deployment,指定容器镜像和服务端口。
  • Service
    :创建一个 Service,使外部能够访问 Locust Web 界面。

结论

通过以上步骤,我们成功在 Kubernetes 中运行了 Locust 和 Selenium,并安装了 Chrome 和 ChromeDriver。确保配置正确的环境变量和 Chrome 选项,可以大大提高在 Kubernetes 环境中的稳定性。如果您有更多需求,可以根据项目的具体情况进行扩展和调整。

使用duxapp,我是如何实现快速完成项目开发的?

像下面这个例子,这个项目有140多个页面,但是真实的开发时间,在熟练使用duxapp的情况下,不会超过两周,并且可以将它兼容APP、小程序、H5
image
image
image

这里仅展示了其中一部分页面,这个项目主要包含下这些功能

  • 购物订单流程
  • 售后退换
  • 文章发布
  • 门店功能
  • 送货人员功能
  • 经销商功能
  • 扫码抽奖
  • 等其他功能

下面我将详细介绍使用了哪些方法,来快速完成项目

使用全局样式快速布局页面

以这个门店管理首页为例,你可以看到我并没有导入一个
scss
文件,但是我使用了很多的
className
,这些就是用的全局样式,这全局样式都是
duxapp模块提供的

import { TopView, Header, Card, Divider, ScrollView, Tag, Row, Image, Column, Text, px, nav, ModalForm, DatePicker, dayjs } from '@/duxui'
import { ArecaIcon, CmsIcon, TipNav, useRequest } from '@/arecaShop'
import { useState } from 'react'

export default function StoreManage() {

  const [date, setDate] = useState()

  const [{ info = {}, sku = {}, distribution = {} }] = useRequest({
    url: 'store/index',
    toast: true,
    data: {
      date
    }
  }, { reloadForShow: true })

  return (
    <TopView>
      <Header title='门店详情' />
      <ScrollView>
        <Card className='p-2 gap-2' margin disableMarginBottom>
          <Row className='gap-2'>
            <Image className='r-2'
              src={info.image}
              style={{ width: px(148) }}
              square
            />
            <Column justify='between' grow>
              <Row justify='between' grow>
                <Text bold size={4}>{info.name}</Text>
                <ArecaIcon name='shezhi' className='text-c1 text-s7'
                  onClick={() => nav('arecaShop/storeManage/info')}
                />
              </Row>
              <Column>
                <Text color={2} size={1}>联系人:{info.contact}</Text>
                <Row grow justify='between' items='center'>
                  <Text color={2} size={1}></Text>
                  <Tag type='secondary' size='s' radiusType='round' onClick={() => nav(`tel:${info.distributor_tel}`)}>
                    <ArecaIcon className='text-white text-s2' name='shiwu-shouji' /> 一键报单进货
                  </Tag>
                </Row>
              </Column>
            </Column>
          </Row>
          <Divider />
          <Row>
            <Text color={2} size={1} shrink>门店地址:</Text>
            <Text bold size={1}>{info.area}{info.address}</Text>
          </Row>
        </Card>
        <TipNav title='货品统计' url='arecaShop/storeManage/mallTotal'>
          <Row className='items-center' grow>
            <ArecaIcon className='text-secondary' name='promot_tips' />
            <Text type='secondary' size={2}>{dayjs().format('MM/DD HH:mm')} 更新</Text>
          </Row>
        </TipNav>
        <Card margin disableMarginBottom className='gap-2'>
          <Text color={3} size={2}>历史进货数量(小包):{sku.in}</Text>

          <Row justify='between' className='gap-2'>
            <Column grow className='bg-page r-2 p-3 gap-1'>
              <Text color={3} size={2}>历史销售数量(小包)</Text>
              <Text bold size={40}>{sku.out}</Text>
            </Column>
            <Column grow className='bg-page r-2 p-3 gap-1'>
              <Text color={3} size={2}>历史剩余数量(小包)</Text>
              <Text bold size={40}>{sku.supply}</Text>
            </Column>
          </Row>
          <Text size={22} type='secondary'>*销量及剩余仅供参考:记录消费者扫码数量,可能与实际结果有出入</Text>
        </Card>
        <TipNav title='配送记录'
          right={<ModalForm
            title='月份'
            renderForm={<DatePicker
              mode='month'
            />}
            childPropsValueKey='value'
            value={date}
            onChange={setDate}
          >
            <DateChild />
          </ModalForm>}
        ></TipNav>
        <Card margin disableMarginBottom className='gap-2'>
          <Text color={3} size={2}>门店配送(中包):{distribution.middle_num}</Text>

          <Row justify='between' className='gap-2'>
            <Column grow className='bg-page r-2 p-3 gap-1'
              onClick={() => nav('arecaShop/storeManage/emptyRecovery', { type: 'store' })}
            >
              <Text color={3} size={2}>已结算空袋(小包)</Text>
              <Row items='center' className='gap-1'>
                <Text bold size={40}>{distribution.recycle_num}</Text>
                <CmsIcon className='text-c3 text-s5' name='direction_right' />
              </Row>
            </Column>
            <Column grow className='bg-page r-2 p-3 gap-1'
              onClick={() => nav('arecaShop/storeManage/deliveryList')}
            >
              <Text color={3} size={2}>门店已退货数(小包)</Text>
              <Row items='center' className='gap-1'>
                <Text bold size={40}>{distribution.refund_num}</Text>
                <CmsIcon className='text-c3 text-s5' name='direction_right' />
              </Row>
            </Column>
          </Row>
        </Card>
      </ScrollView>
    </TopView>

  )
}

const DateChild = ({ value, onClick }) => {
  return <Row items='center' className='r-2 ph-2 bg-white gap-1' onClick={onClick}>
    <Text color={value ? 1 : 3}>{value || '请选择'}</Text>
    <ArecaIcon name='rili1' className='text-c3' />
  </Row>
}

使用UI库

在上面的示例中已经展示了如何使用UI库的组件,可以看到使用了UI库的组件,在结合全局样式,你会发现,似乎不需要编写scss就能很好的完成页面布局了

这些组件都是
duxui UI库
提供的

使用List组件完成列表页面

以下面这个列表页面为例,List组件帮你完成了数据请求、自动分页、下拉刷新、自动刷新等功能,你只需要关注你列表中的每一项是如何编写的

import { TopView, Header, Card, Tab, Row, Image, Column, Text, px, Empty, DatePicker, SelectorPicker, ModalForm, Button } from '@/duxui'
import { ArecaIcon, List, usePageData, nav } from '@/arecaShop'
import { useState } from 'react'

export default function DeliveryList() {

  const [users] = usePageData('store/salesman')

  const [user, setUser] = useState()

  const [type, setType] = useState(0)

  return (
    <TopView>
      <Header title='配送记录' />
      <Tab className='bg-white' value={type} onChange={setType}>
        <Tab.Item title='门店配送' paneKey={0} />
        <Tab.Item title='门店退货' paneKey={1} />
      </Tab>
      <Row className='ph-3 mt-3' items='center' justify='between'>

        <ModalForm
          title='业务员'
          renderForm={<SelectorPicker range={users} nameKey='nickname' valueKey='id' />}
          childPropsValueKey='value'
          value={user}
          onChange={setUser}
        >
          <SelectChild />
        </ModalForm>

      </Row>

      <List
        key={type}
        renderItem={Item}
        renderEmpty={<Empty title='暂无记录' />}
        url={type ? 'store/refund' : 'store/distribution'}
        data={{
          salesman_id: user
        }}
      />
    </TopView>
  )
}

const SelectChild = ({ value, ...props }) => {

  return (
    <Row items='center'  {...props}>
      <Text size={2}>{value || '全部业务员'}</Text>
      <ArecaIcon name='arrow_down_fill' className='text-s4 text-c2' />
    </Row >
  )
}

const Item = ({ item }) => {
  return <Card margin disableMarginBottom className='gap-3'>
    <Row items='center'>
      <ArecaIcon className='text-c1 text-s7' name='store' />
      <Text size={2} style={{ marginLeft: px(4) }}>{item.store_name}</Text>
      <ArecaIcon className='text-c3 text-s5' name='you2' />
    </Row>
    {
      item.goods.map(good => <Row className='gap-3' key={good.id}>
        <Image style={{ width: px(160) }} square className='r-2' src={good.image} />
        <Column grow justify='between'>
          <Text size={2} bold numberOfLines={2}>{good.title}</Text>
          <Text size={2} color={3}>规格:{good.spec}</Text>
          <Text size={2} color={3}>数量:{good.num}</Text>
        </Column>
      </Row>)
    }

    <Column className='r-2 bg-page p-3 gap-2'>
      <Row>
        <Row grow items='center'>
          <ArecaIcon className='text-secondary text-s7' name='man_mine' />
          <Text size={2}>{item.salesman_name || '-'}</Text>
        </Row>
        <Row grow items='center'>
          <ArecaIcon className='text-secondary text-s7' name='shiwu-shouji' />
          <Text size={2}>{item.salesman_tel || '-'}</Text>
        </Row>
      </Row>
      <Row>
        <Row grow items='center'>
          <ArecaIcon className='text-secondary text-s7' name='map' />
          <Text size={2}>{item.area}</Text>
        </Row>
        <Row grow items='center'>
          <ArecaIcon className='text-secondary text-s7' name='shijian1' />
          <Text size={2}>{item.created_at}</Text>
        </Row>
      </Row>
    </Column>
  </Card>
}

使用hook快速请求数据

这里是指的使用
useRequest
替代
request
快速获取数据,而不需要在编写具体请求逻辑

import { TopView, Header, Card, Text, ScrollView, Space, HtmlView, useRoute } from '@/duxui'
import { useRequest } from '@/arecaShop'

export default function HelpDetail() {

  const { params } = useRoute()

  const [detail] = useRequest(`tools/magic/help/${params.id}`)

  return (
    <TopView isSafe>
      <Header title='帮助详情' />
      <ScrollView >
        <Card shadow={false} margin>
          <Space size={32}>
            <Text bold size={42} >{detail.title}</Text>
            <HtmlView html={detail.content} />
          </Space>
        </Card>
      </ScrollView>
    </TopView>
  )
}

使用现有模块

这个项目中的 购物订单流程、售后退换、文章发布等功能,在之前的模块中都已经是开发过的功能,并且他们都是成熟可用的,我们只需要直接使用他们就行了

{
  "name": "arecaShop",
  "description": "大工匠槟榔商城",
  "version": "1.0.0",
  "dependencies": [
    "duxcmsMall",
    "amap"
  ]
}

在我这个项目模块的配置文件中,将
duxcmsMall
添加到依赖中,这是商城模块他提供了商品、订单、售后、购物车、收货地址等功能

因为当前项目需求是积分兑换商品,但是默认的商城不支持积分功能,下面介绍是使用渲染钩子来修改为积分商品

使用渲染钩子对商城进行修改

使用RenderHook,将商城购物流程修改为积分兑换流程,这个文件需要在模块入口文件中导入

import { mallHook, orderHook, Price } from '@/duxcmsMall'
import { Card, contextState, Row, Text } from '@/duxui'

const ListPrice = ({ item, children }) => {
  if (item.type === 'point') {
    return <Text bold size={4} type='danger'>{item.sell_point}积分</Text>
  }
  return children
}

mallHook.add('goods.list.item.sellPrice', ListPrice)
mallHook.add('MallCateQuick.item.sellPrice', ListPrice)
mallHook.add('MallList.item.sellPrice', ListPrice)

mallHook.add('detail.info.price', ({ children }) => {

  const [data] = contextState.useState()

  if (data.type !== 'point') {
    return children
  }

  return <Row className='gap-2' items='center'>
    <Text size={40} bold color={4}>
      {data.sell_point}积分
    </Text>
    <Price size={1} delete color={4} className='mt-2'>{data.market_price}</Price>
  </Row>
})

mallHook.add('GoodsSpec.price', ({ children, data, item }) => {
  if (data.type !== 'point') {
    return children
  }
  return <Text size={48} bold type='danger'>
    {item?.sell_point || data.sell_point}积分
  </Text>
})

orderHook.add('order.create.data.total', ({ store }) => {

  // const isPoint = store.items.some(v => v.type === 'point')

  return <>
    <Row items='center' justify='between'>
      <Text bold>商品金额</Text>
      <Price color={2}>{store.total.order_price}</Price>
    </Row>
    <Row items='center' justify='between'>
      <Text bold>运费</Text>
      <Price color={2}>{store.total.delivery_price}</Price>
    </Row>
    {
      store.discount.map(item => <Row key={item.name} items='center' justify='between'>
        <Text bold>{item.desc}</Text>
        <Text type='danger'>-¥{item.price}</Text>
      </Row>)
    }
    {/* {+store.total.discount_price > 0 && <Row items='center' justify='between'>
      <Text bold>{isPoint ? '积分抵扣' : '优惠'}</Text>
      <Price type='danger'>{-store.total.discount_price}</Price>
    </Row>} */}
  </>
})

orderHook.add('order.detail.total', () => {
  const [{ data }] = contextState.useState()

  return <Card margin disableMarginBottom className='gap-3'>
    <Row items='center' justify='between'>
      <Text color={2}>订单总额</Text>
      <Price bold color={1}>{data.order_price}</Price>
    </Row>
    <Row items='center' justify='between'>
      <Text color={2}>运费</Text>
      <Price bold color={1}>{data.delivery_price}</Price>
    </Row>
    {
      data.discount_data?.map(item => <Row key={item.name} items='center' justify='between'>
        <Text color={2}>{item.desc}</Text>
        <Text bold type='danger'>-¥{item.price}</Text>
      </Row>)
    }
    <Row items='center' justify='between'>
      <Text color={2}>实付款</Text>
      <Price bold type='primary'>{data.pay_price}</Price>
    </Row>
  </Card>
})

总结

上面提到的就是用于快速开发的主要方法,当你熟练掌握这些方法后,你的开发速度将一骑绝尘

当然这些上面提到这些并不是全部,你可以阅读开发文档,从中获取更多的使用方法

请前往开发文档查看详细教程

开发文档:
http://duxapp.cn/

GitHub:
https://github.com/duxapp

前一篇:《全面解释人工智能LLM模型的真实工作原理(一)》

序言:
在上一篇文章中,我们从原理上构建了一个识别“叶子”和“花朵”的神经网络,并详细讲解了它的工作过程。这包括对输入数字逐个与权重相乘后求和,加上偏置值,最后通过非线性处理和统计分布计算来得出输出。这些操作使用了简单的数学运算(乘法、加法和非线性处理)。本节的重点是解答神经网络的权重和偏置值是如何得到的以及最关键的概念:如何让神经网络输出chatGPT一样的句子。为了让神经网络学到合适的权重和偏置,我们需要提供大量的学习数据(如大量的“叶子”和“花朵”图片),让网络在学习过程中调整每个神经元的权重和偏置值,最终实现正确分类。(
请动一下您的小手,订阅作者!

如何训练这个神经网络(模型)?

在上例中,我们为了测试,给模型预设了合适的权重和偏置,这样才能得到准确的输出。但在实际应用中,权重和偏置值是如何获得的呢?获得合适的‘权重’和‘偏置’这个过程就称为“训练模型”或“训练神经网络”,也可以理解为“人工智能的自我学习”;没错,这个过程就是“训练AI”。人类需要做的就是为模型提供优质数据来进行训练。

假设我们收集了一些数据,包括各种类型的“叶子”和“花朵”。然后,我们用工具将它们的颜色和体积转换成数字,给每个数据样本贴上“叶子”或“花朵”的标签(给数据取名字就称为“标注数据”),最终这些数据组成了我们的“训练数据集”。

训练神经网络的工作原理如下:

  1. 初始化权重

首先,从随机数开始,将神经元的每个参数/权重设为一个随机数。(启动训练程序时,计算机内存中未初始化的都是随机数,一般无须特别设定)

  1. 输入数据并获得初始输出

我们给神经网络输入“叶子”的数据表示(如 R=32,G=107,B=56,Vol=11.2),期望输出层第一个神经元的值大于第二个神经元的值,表示识别出“叶子”。假如预期“叶子”神经元的值是0.8,代表“花”的神经元值是0.2。

  1. 计算损失

因为初始权重是随机的,实际输出往往和预期有差异。比如,两个神经元的初始输出分别是0.6和0.4。我们可以通过求差并将差值平方相加计算损失:(0.8 - 0.6)² + (0.2 - 0.4)² = 0.04 + 0.04 = 0.08。理想情况下,我们希望损失接近于零,也就是“最小化损失”。

  1. 计算梯度并更新权重

计算每个权重对损失的影响(称为梯度),看向哪个方向调整才能减少损失。梯度指示了每个参数的变化方向——权重会朝损失减少的方向略微调整一点。这个过程称为“梯度下降”。

  1. 重复迭代

持续重复这些步骤,通过不断更新权重,使得损失逐步减少,最终得到一组“训练好的”权重或参数。这就是神经网络的训练过程,称为“梯度下降”。

补充说明

• 多个训练样本

训练中通常会使用多个样本。微调权重以最小化某个样本的损失可能会导致其他样本的损失增大。为了解决这个问题,通常会计算所有样本的平均损失,并基于平均损失的梯度来更新权重。每次完整的样本循环称为“一个 epoch”,多个 epoch 的训练可以帮助逐步找到更优的权重。

• 自动计算梯度

实际上,无需手动微调权重来计算梯度,数学公式可以直接推导出每个参数的最佳调整方向。例如,如果上一步权重为 0.17,且神经元的输出希望增大,那么将权重调整为 0.18 可能更有效。

在实践中,训练深度网络是一个复杂的过程,训练中可能会遇到梯度失控的情况,例如梯度值趋于零或趋向无穷大,这分别称为“梯度消失”和“梯度爆炸”问题。虽然上述的损失定义有效,但在实际应用中,通常会使用更适合特定任务的损失函数来提高训练效果。

这些原理怎样帮助神经网络生成语言?

请记住,神经网络只能接收输入一组数字,基于训练好的参数进行数学运算,最后输出另一组数字。关键在于如何解释这些数字,并通过训练来自动调整参数。如果我们能够把两组数字解释为“叶子/花朵”或“一小时后是晴天或雨天”,同样也可以将它们解释为“句子的下一个字符”。

但是,英语字母远不止两个,所以我们需要将输出层的神经元数量扩展,例如扩展到26个以上的神经元(再加上一些符号,如空格、句号等)。每个神经元对应一个字母或符号,然后我们在输出层中找出数值最大的神经元,并将其对应的字符作为输出字符。现在我们就有了一个可以接收输入并输出字符的网络。

如果我们给神经网络输入“Humpty Dumpt”这个字符串,然后让它输出一个字符,并将其解释为“网络预测到的下一个字符”,我们可以通过训练,确保网络在收到这样的字符串“Humpty Dumpt”输入时输出字母“y”,从而达到我们想要的结果“Humpty Dumpty”。

不过,这里有一个问题:如何将字符串输入到网络中?毕竟,神经网络只接受数字!通常实践中我们可以通过“one-hot编码”或其他编码方法将字符串转换成数值数组,使其可以被神经网络理解和处理。

这里我们用一个最简单的解决方案来编码:直接为每个字符分配一个数字。例如,a=1,b=2,依此类推。现在我们可以输入“humpty dumpt”并训练网络输出“y”。网络的工作过程如下:

先在神经网络的输入层输入一串句子(字符串),它将会在输出层预测下一个字符。这样的方法可以帮助我们构建完整的句子。例如,当我们预测出“y”后,可以将这个“y”添加到前面输入的字符串尾部,并再次送回神经网络的输入层,让它预测下一个字符。如果训练得当,网络会预测出一个空格;如此循环下去,最终生成出完整的句子:“Humpty Dumpty sat on a wall”。这样,我们就得到了一个生成式 AI(人工智能语言模型),神经网络现在可以生成人类的自然语言了!

当然,在真实应用中例如chatGPT,我们不会使用这种简单的字符编号方法。在后文中,我们会介绍一种更合理的编码方式。如果你迫不及待,可以查看附录中的“编码”部分。

细心的读者可能会注意到,我们无法直接输入“Humpty Dumpty”,因为如图所示,输入层只有12个神经元,对应于“humpty dumpt”中的每个字符(包括空格),并没有多余的神经元留给字母‘y’输入了。那么,如何在下一步中加入“y”呢?如果在输入层加上第13个神经元,就需要重新调整整个网络,这显然不太现实。解决方案很简单:我们可以将最早的字符“h”剔除,保留最近的12个字符输入。例如,我们输入“umpty dumpty”,网络会预测出一个空格;然后我们输入“mpty dumpty ”,网络会输出“s”,如此循环下去,过程如下所示:

这种方法有个问题,即当我们输入“ sat on the wal”时,会丢失之前的许多信息。那么,现代顶尖神经网络是如何解决的呢?原理基本相似。神经网络的输入的长度是固定的(取决于输入层的大小),这种长度称为“上下文长度”,即网络用来预测后续内容的参考范围。现代网络的上下文长度可以很长(通常达到几万甚至几十万个字符。例如,ChatGPT的4o模型支持12.8万个字符,Claude则支持25.6万个字符。这意味着它们在输入层中使用了超过10万个神经元来接收用户的输入。试想一下,上千亿参数意味着有多少神经元在参与运算?),这对提升效果非常有帮助。尽管某些方法允许输入无限长度的序列,但固定上下文长度较大的模型在性能上已经优于这些方法。

细心的读者可能还会注意到,我们在输入和输出端对同一个字母的解释方式不同!例如,输入“h”时我们用数字8表示它,但在输出层,我们并不直接要求模型输出数字8来代表“h”,而是生成26个数值,并选择其中最大值对应的字母作为输出。如果第8个数值最大,我们将其解释为“h”。为什么不在两端使用相同的表示方式呢?事实上,这是为了构建更有效的模型——不同的输入和输出解释方式为模型性能的提升提供了更多可能。实践表明,这种不同的表示方式对语言生成更有效。实际上,我们在输入端的数字表示方式也并非最佳,稍后会介绍更优的方法。

本节是搞明白chatGPT输出人类自然语言句子的核心原理,希望感兴趣的朋友如果没有搞明白,多读几篇或者在评论区留言与作者交流,我会毫无遗漏的回答所有的评论。

未完待续…

最近,在使用
Rust
时遇到了
Reborrow
的概念,记录下来以备以后参考。

1. 起因

起因准备对数据进行
Min-Max
标准化处理,也就是将一系列数据映射到一个新的范围。

首先,需要遍历数据,找出其中的
最大值

最小值
,然后通过公式改变原始数据集的值。

Min-Max
公式:标准化后的值 = (原始值 - 最小值) / (最大值 - 最小值)

简化后的代码如下:

fn main() {
    let mut values = vec![10.5, 22.3, 103.5, 45.75];
    let v = &mut values;
    println!("原始数据: {:#?}", v);

    let mut max = f64::MIN;
    let mut min = f64::MAX;

    for n in v {
        if *n > max {
            max = *n;
        }
        if *n < min {
            min = *n;
        }
    }

    println!("max is {}", max);
    println!("min is {}", min);

    println!("开始Min-Max标准化处理...");
    for n in v {
        *n = (*n - min) / (max - min);
    }

    println!("处理后数据: {:#?}", values);
}

运行时有如下错误:

error[E0382]: use of moved value: `v`                                                                     
   --> src/main.rs:22:14
    |
3   |     let v = &mut values;
    |         - move occurs because `v` has type `&mut Vec<f64>`, which does not implement the `Copy` trai
t
...
9   |     for n in v {
    |              - `v` moved due to this implicit call to `.into_iter()`
...
22  |     for n in v {
    |              ^ value used here after move
    |

大概是
第9行
遍历
v
的找出最大值和最小值时候,
可变借用v
的使用权已经转移了,

所以在
第22行

再次遍历v
去修改值的时候,出现错误。

这里,因为
Vector
没有实现
Copy Trait
,所以它的可变借用在第一次遍历时,由于隐式的调用了
.into_iter()
,所有权发生了转移。

如果想多次遍历
Vector
,可以使用它的不可变借用,比如定义
let v = &values;

那么,就可以多次遍历
v
,因为不可变借用都实现了
Copy Trait

但是,我第二次遍历
v
的时候,还需要修改其中的值,所以必须定义为可变借用
let v = &mut values;

通过查询资料,发现
Reborrow
的机制可以实现上面的需求。

2. Reborrow概念

借用(
Borrow
)是
Rust
中的一个重要概念,它是允许代码访问某个值而不获取其所有权的一种机制。


Reborrow
则是指在一个已存在的借用基础上创建一个新的借用,

这个新的借用可以是不可变的,也可以是可变的(前提是原始借用是可变的,并且没有其他借用存在)。

总的来说,
Reborrow
通过在已存在的借用上创建新的借用,从而扩展引用的生命周期并在更广泛的作用域内安全地访问值。

3. 解决方法

下面通过实践来检验对
Reborrow
概念的理解。

回到第一节中遇到的问题,解决方式就是在第一次遍历
v
时(
第9行
),不要把所有权转移出去,

这样,第二次遍历
v

第22行
)的时候,就不会报出
"value used here after move"
的错误。

根据
Reborrow
的机制,我们在
第9行
可以
Reborrow
可变借用
v
,这样转移出去的是被再次借用的
v
,而不是
v
本身。

改变方法很简单,
第9行
改为
for n in &*v {
即可,也就是先
还原v
(
*v
),然后
Reborrow
(
&*v
)。

修改后再次运行代码:

$  cargo run

原始数据: [
    10.5,
    22.3,
    103.5,
    45.75,
]
max is 103.5
min is 10.5
开始Min-Max标准化处理...
处理后数据: [
    0.0,
    0.12688172043010754,
    1.0,
    0.3790322580645161,
]

values
中的数据可以正常转换了。

注意,这里是将
v
Reborrow成一个不可变借用
&*v
,因为我第一次遍历时不需要改变
v

如果想
v
Reborrow成一个可变借用,可以写成:
&mut *v

buck电路

buck电路是直流的降压电路,我们下面给大家讲下,如何把12V的直流电压降压成5V的直流电压

1、buck电路拓扑:12V----->5V

2、降压原理

a、开关闭合,电流走向

电源的正极---->开关---->电感----->(电容和负载)----->电源负极

这里由于二极管是单相导通的,所以此时二极管是没有电流通过的

大家要注意,此时的电流也会给电容充电

这里最重要是电感,由于电感上的电流是不能突变的,所有流经电感的电流是慢慢增大,通过欧姆定律:电压=电阻 * 电流,所以负载电压是慢慢增大的。

当负载的电压超过5V的时候,此时我们会断开开关

b、开关断开,电流分析

断开开关,流过电感的电流会突然变小,由于电感的特性,流经电感的电流是不能突变的,所以此时电感上是 左负右正。在电感上的电压从左正右负变为左负右正的瞬间,电路中是没有电流的,所以此时的一瞬间,给负载供电的电容放电给负载供电

电感的正极---->(负载,电容)---->二极管---->电感的负极

电感放电的瞬间,电流是很大的,虽然电感的磁能转换电能,磁能越来越小,电能也越来越小,电流也越来越小,通过欧姆定律:电压=电阻 * 电流,所以负载电压是慢慢减小的,当减少到一定的程度,我们再次闭合开关。闭合的瞬间,电感的极性会变化,这个变化的一瞬间,又是由电容给负载供电

所以整个周期负载的电压变化是下面这样的,有效值大概就是在5V左右

我们是怎么控制输出的电压是5V的,这里就需要引入另外一个概念,占空比

占空比=输出电压/输入电压

占空比=5/12,也就是5/12的时间开关是闭合的,7/12的时间是开关的断开。这里的时间是很短的,以微妙为单位,用普通的开关是不行的,我们一般用MOS管,IGBT等代替

boost电路

1、boost电流拓扑:5V---->12V

boost电路是直流的升压压电路,我们下面给大家讲下,如何把5V的直流电压升压成12V的直流电压

2、升压原理

a、开关闭合,电流走向

开关闭合后,相当于短路,所以右半部分相当于短路状态,此时电流流过电感,形成左正右负的电源,电能转换为磁能

b、开关断开,电流走线

开关断开,由于阻抗大于开关闭合的时候,所以流过电感的电流会越来越小,电感为了阻止电流变小,会形成左负右正的电压,此时就和电源串联,使得最终输出的电压是大于5V的,这里就是boost电流升压的核心

c、开关再次闭合,给负载供电的电容放电,虽然电容的放电,电压会越来越小,当小于12V的时候,立刻断开

d、最终实现升压,最终的输出电压和什么有关系呢?又是占空比:D

Vout=Vin/(1-D),可以得出,如果要升压到12V,则占空比D=7/12,所以开关闭合的时间站7/12,开关断开的时间占5/12

最终实现输出的电压在12V上下波动,有效值就是12V