2024年11月

书接上回,我们继续来聊聊图的遍历与实现。

01
、遍历

在图的基本功能中有个很重要的功能——遍历,遍历顾名思义就是把图上所有的点都访问一遍,具体来说就是从一个连通的图中某一个点出发,沿着边访问所有的点,并且每个点只能访问一遍。

下面我们介绍两种常见的遍历方式:深度优先遍历(DFS)和广度优先遍历(BFS)。

1、深度优先遍历

如果我们把边当作路,深度优先遍历就是路一直往下走,直到没路了再返回走其他路。其实优点像树的先序遍历从根节点沿着子节点一直向下直到叶子节点再调头。

下面我们梳理一下深度优先遍历大致分为以下几个步骤:

(1)从图中任意一个点A出发,并访问点;

(2)找出点A的第一个未被访问的邻接点,并访问该点;

(3)以该点为新的点,重复步骤(2),直至新的邻接点没有未被访问的邻接点;

(4)返回前一个点并依次访问前一个点为未被访问的其他邻接点,并访问该点;

(5)重复步骤(3)和(4),直至所有点都被访问过;

如上图演示了从点A出发进行深度优先遍历过程,其中红色虚线表示前进路线,蓝色虚线表示回退路线。最后输出:A->B->E->F->C->G->D。

2、广度优先遍历

如果说深度优先遍历是找到一条路一直走到底,那么广度优先遍历就是先把所有的路走一步再说。其实优点像树的层次遍历从根节点出发先遍历其子节点然后再遍历其孙子节点直至遍历完所有节点。

下面我们梳理一下广度优先遍历大致分为以下几个步骤:

(1)从图中任意一点A出发,并访问点A;

(2)依次访问点A所有未被访问的邻接点,访问完邻接点后,然后按邻接点顺序把邻接点作为新的出发执行步骤(1);

(3)重复步骤(1)和(2)直至所有点都被访问到。

如上图演示了从点A出发进行广度优先遍历过程,其中红色虚线表示前进路线。最后输出:A->B->C->D->E->F->G。

02
、实现(邻接矩阵)

下面我们就以邻接矩阵的存储方式实现一个无向图。

1、定义

根据图的定义,我们需要定义点集合、边集合两个私有变量用于存储核心数据,为了操作访问我们再定义点数量和边数量两个私有变量,代码如下:

//点集合
private T[] _vertexArray { get; set; }
//边集合
private int[,] _edgeArray { get; set; }
//点数量
private int _vertexCount;
//边数量
private int _edgeCount { get; set; }

2、初始化 Init

此方法主要是初始化上面定义的私有变量,同时确定点集合大小,具体代码如下:

//初始化
public MyselfGraphArray<T> Init(int length)
{
    //初始化指定长度点集合
    _vertexArray = new T[length];
    //初始化指定长度边集合
    _edgeArray = new int[length, length];
    //初始化点数量
    _vertexCount = 0;
    //初始化边数量
    _edgeCount = 0;
    return this;
}

3、获取点数量 VertexCount

我们可以通过点数量私有变量快速获取图的点数量,代码如下:

//返回点数量
public int VertexCount
{
    get
    {
        return _vertexCount;
    }
}

4、获取边数量 EdgeCount

我们可以通过边数量私有变量快速获取图的点数量,代码如下:

//返回边数量
public int EdgeCount
{
    get
    {
        return _edgeCount;
    }
}

5、获取点索引 GetVertexIndex

该方法是通过点元素获取其索引值,具体代码如下:

//返回指定点元素的索引   
public int GetVertexIndex(T vertex)
{
    if (vertex == null)
    {
        return -1;
    }
    //根据值查找索引
    return Array.IndexOf(_vertexArray, vertex);
}

6、获取点元素 GetVertexByIndex

该方法通过点索引获取点元素,具体代码如下:

//返回指定点索引的元素
public T GetVertexByIndex(int index)
{
    //如果索引非法则报错
    if (index < 0 || index > _vertexArray.Length - 1)
    {
        throw new InvalidOperationException("索引错误");
    }
    return _vertexArray[index];
}

7、插入点 InsertVertex

插入点元素时,我们需要先通过点元素获取其索引,如果索引已存在或者点集合已经满了则直接返回,否则添加点元素同时更新点数量,具体代码如下:

//插入点
public void InsertVertex(T vertex)
{
    //获取点索引
    var index = GetVertexIndex(vertex);
    //如果索引大于-1说明点已存在,则直接返回
    if (index > -1)
    {
        return;
    }
    //如果点集合已满,则直接返回
    if (_vertexCount == _vertexArray.Length)
    {
        return;
    }
    //添加点元素,并且更新点数量
    _vertexArray[_vertexCount++] = vertex;
}

8、插入边 InsertEdge

插入边时可以同时指定边的权值。我们首先需要把两个点元素转换为点索引,同时验证索引,验证不通过则直接返回。否则开始添加边,因为无向图的特性,所以需要添加两点索引相反的边。同时更新边数量,具体代码如下:

//插入边
public void InsertEdge(T vertex1, T vertex2, int weight)
{
    //根据点元素获取点索引
    var vertexIndex1 = GetVertexIndex(vertex1);
    //如果索引等于-1说明点不存在,则直接返回
    if (vertexIndex1 == -1)
    {
        return;
    }
    //根据点元素获取点索引
    var vertexIndex2 = GetVertexIndex(vertex2);
    //如果索引等于-1说明点不存在,则直接返回
    if (vertexIndex2 == -1)
    {
        return;
    }
    //更新两点关系,即边信息
    _edgeArray[vertexIndex1, vertexIndex2] = weight;
    //用于无向图,对于有向图则删除此句子
    _edgeArray[vertexIndex2, vertexIndex1] = weight;
    //更新边数量
    _edgeCount++;
}

9、获取边权值 GetWeight

该方法可以获取边的权值,权值可以根据需要在插入边方法中设置,需要对输入的点进行验证,如果点不存在则报错,具体代码如下:

//返回两点之间边的权值
public int GetWeight(T vertex1, T vertex2)
{
    //根据点元素获取点索引
    var vertexIndex1 = GetVertexIndex(vertex1);
    //如果索引等于-1说明点不存在
    if (vertexIndex1 == -1)
    {
        //如果未找到点则报错
        throw new KeyNotFoundException($"点不存在");
    }
    //根据点元素获取点索引
    var vertexIndex2 = GetVertexIndex(vertex2);
    //如果索引等于-1说明点不存在
    if (vertexIndex2 == -1)
    {
        //如果未找到点则报错
        throw new KeyNotFoundException($"点不存在");
    }
    return _edgeArray[vertexIndex1, vertexIndex2];
}

10、深度优先遍历 DFS

深度优先遍历正常有两种实现方法,一种是使用递归调用,一种是使用栈结构实现,下面我们使用递归的方式来实现。

因为我们需要保证每个点只会被访问一次,因此需要定义一个数组用来记录元素已经被访问过。我们这里是以无向图为例,因为无向图的对称性,索引我们选用一维数组即可满足记录被访问元素,而如果是有向图我们则需要使用二维数组记录被访问元素。

具体代码如下:

//深度优先遍历
public void DFS(T startVertex)
{
    //根据点元素获取点索引
    var startVertexIndex = GetVertexIndex(startVertex);
    //如果索引等于-1说明点不存在
    if (startVertexIndex == -1)
    {
        //如果未找到点则报错
        throw new KeyNotFoundException($"点不存在");
    }
    //定义已访问标记数组
    //因为无向图对称特性因此一维数组即可
    //如果是有向图则需要定义二维数组
    var visited = new bool[_vertexCount];
    DFSUtil(startVertexIndex, visited);
    Console.WriteLine();
}
//深度优先遍历
private void DFSUtil(int index, bool[] visited)
{
    //标记当前元素已访问过
    visited[index] = true;
    //打印点
    Console.Write(_vertexArray[index] + " ");
    //遍历查找与当前元素相邻的元素
    for (var i = 0; i < _vertexCount; i++)
    {
        //如果是相邻的元素,并且元素未被访问过
        if (_edgeArray[index, i] == 1 && !visited[i])
        {
            //则递归调用自身方法
            DFSUtil(i, visited);
        }
    }
}

11、广度优先遍历 BFS

广度优先遍历可以借助队列来实现。首先把起始点添加入队列,然后把点出队列,同时把该点的所有邻接点添加入队列,循环往复,一直到把所有元素处理完为止。

//广度优先遍历
public void BFS(T startVertex)
{
    //根据点元素获取点索引
    var startVertexIndex = GetVertexIndex(startVertex);
    //如果索引等于-1说明点不存在
    if (startVertexIndex == -1)
    {
        //如果未找到点则报错
        throw new KeyNotFoundException($"点不存在");
    }
    //定义已访问标记数组
    //因为无向图对称特性因此一维数组即可
    //如果是有向图则需要定义二维数组
    var visited = new bool[_vertexCount];
    //使用队列实现广度优先遍历
    var queue = new Queue<int>();
    //将起点入队
    queue.Enqueue(startVertexIndex);
    //标记起点为已访问
    visited[startVertexIndex] = true;
    //遍历队列
    while (queue.Count > 0)
    {
        //出队点
        var vertexIndex = queue.Dequeue();
        //打印点
        Console.Write(_vertexArray[vertexIndex] + " ");
        //遍历查找与当前元素相邻的元素
        for (var i = 0; i < _vertexCount; i++)
        {
            //如果是相邻的元素,并且元素未被访问过
            if (_edgeArray[vertexIndex, i] == 1 && !visited[i])
            {
                //则将相邻元素索引入队
                queue.Enqueue(i);
                //并标记为已访问
                visited[i] = true;
            }
        }
    }
    Console.WriteLine();
}


:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。
https://gitee.com/hugogoos/Planner

为了构建生成式AI应用,需要完成两个部分:

  • AI大模型服务:有两种方式实现,可以使用大厂的API,也可以自己部署,本文将采用ollama来构建
  • 应用构建:调用AI大模型的能力实现业务逻辑,本文将采用Spring Boot + Spring AI来实现

Ollama安装与使用

进入官网:
https://ollama.com/
,下载、安装、启动 ollama

具体步骤可以参考我之前的这篇文章:
手把手教你本地运行Meta最新大模型:Llama3.1

构建 Spring 应用

  1. 通过
    spring initializr
    创建Spring Boot应用

  2. 注意右侧选择Spring Web和Spring AI对Ollama的支持依赖

  1. 点击“generate”按钮获取工程

  2. 使用IDEA或者任何你喜欢的工具打开该工程,工程结构如下;

  1. 写个单元测试,尝试在Spring Boot应用里调用本地的ollama服务
@SpringBootTest(classes = DemoApplication.class)
class DemoApplicationTests {

    @Autowired
    private OllamaChatModel chatModel;

    @Test
    void ollamaChat() {
        ChatResponse response = chatModel.call(
                new Prompt(
                        "Spring Boot适合做什么?",
                        OllamaOptions.builder()
                                .withModel(OllamaModel.LLAMA3_1)
                                .withTemperature(0.4)
                                .build()
                ));
        System.out.println(response);
    }

}

运行得到如下输出:

ChatResponse [metadata={ id: , usage: { promptTokens: 17, generationTokens: 275, totalTokens: 292 }, rateLimit: org.springframework.ai.chat.metadata.EmptyRateLimit@7b3feb26 }, generations=[Generation[assistantMessage=AssistantMessage [messageType=ASSISTANT, toolCalls=[], textContent=Spring Boot是一个基于Java的快速开发框架,主要用于创建独立的、生产级别的应用程序。它提供了一个简化的配置过程,使得开发者能够快速构建和部署Web应用程序。

Spring Boot适合做以下几件事情:

1. **快速开发**: Spring Boot提供了一系列的自动配置功能,可以帮助开发者快速创建基本的应用程序,减少手动编写配置代码的时间。
2. **独立运行**: Spring Boot可以作为一个独立的应用程序运行,不需要额外的容器或服务器支持。
3. **生产级别的应用程序**: Spring Boot提供了许多生产级别的特性,例如安全、监控和部署等功能,可以帮助开发者创建高性能、可靠的应用程序。
4. **Web 应用程序**: Spring Boot可以用于创建Web应用程序,包括RESTful API、WebSockets和其他类型的Web应用程序。
5. **微服务架构**: Spring Boot支持微服务架构,允许开发者将一个大型应用程序分解成多个小型服务,每个服务都可以独立运行和部署。

总之,Spring Boot是一个强大的框架,可以帮助开发者快速创建、测试和部署生产级别的应用程序。, metadata={messageType=ASSISTANT}], chatGenerationMetadata=ChatGenerationMetadata{finishReason=stop,contentFilterMetadata=null}]]]

上述样例工程打包放公众号了,如果需要的话,关注"程序猿DD",发送关键词
spring+ollama
获得下载链接。

小结

通过本文的介绍,我们就已经完成了Spring Boot应用与Ollama运行的AI模型之间的对接。剩下的就是与业务逻辑的结合实现,这里读者根据自己的需要去实现即可。

可能存在的一些疑问

  1. 如何使用其他AI模型

通过ollama的
Models
页面,可以找到各种其他模型:

选择你要使用的模型来启动即可。

  1. 如何植入现有应用?

打开上面工程的
pom.xml
,可以看到主要就下面两个依赖:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>

所以,如果要在现有工程引入的话只要引入
spring-ai-ollama-spring-boot-starter
依赖就可以了。

好了,今天的分享就到这里。最近较忙,分享较少,感谢持续的关注与支持
_

如果您学习过程中如遇困难?可以加入我们超高质量的
技术交流群
,参与交流与讨论,更好的学习与进步!

一、关于条件构造器(Wrapper)

1.1 简介

MyBatis-Plus 提供了一套强大的条件构造器(Wrapper),用于构建复杂的数据库查询条件。Wrapper 类允许开发者以链式调用的方式构造查询条件,无需编写繁琐的 SQL 语句,从而提高开发效率并减少 SQL 注入的风险。

edae4c45-b7c2-4e1c-a975-ff823dacb29c

1.2 发展

  1. 核心功能的发展


    • 从早期的MyBatis-Plus版本开始,条件构造器(Wrapper)就已经作为核心功能之一,用于构建复杂的数据库查询条件。随着版本的迭代,条件构造器的功能不断增强,提供了更多的方法来支持各种查询操作,如
      eq
      ​(等于)、
      ne
      ​(不等于)、
      gt
      ​(大于)、
      lt
      ​(小于)等。
  2. 链式调用的优化


    • 条件构造器支持链式调用,这使得代码更加简洁和易读。随着MyBatis-Plus的发展,链式调用的流畅性和易用性得到了进一步的优化,使得开发者可以更加方便地构建复杂的查询条件。
  3. Lambda表达式的引入


    • 随着Java 8的普及,MyBatis-Plus引入了基于Lambda表达式的条件构造器,如
      LambdaQueryWrapper
      ​和
      LambdaUpdateWrapper
      ​,这使得开发者可以使用更加现代的编程方式来构建查询和更新条件,提高了代码的可读性和安全性。
  4. 功能扩展


    • MyBatis-Plus条件构造器的功能不断扩展,新增了许多方法,如
      eqSql
      ​、
      gtSql
      ​、
      geSql
      ​、
      ltSql
      ​、
      leSql
      ​等,这些方法允许开发者直接在条件构造器中嵌入SQL片段,提供了更高的灵活性。
  5. 性能优化


    • 随着MyBatis-Plus的发展,条件构造器在性能上也得到了优化。通过减少不必要的SQL拼接和优化条件构造逻辑,提高了查询的效率。
  6. 易用性的提升


    • MyBatis-Plus不断改进条件构造器的易用性,例如通过提供更多的方法来支持不同的查询场景,如
      groupBy
      ​、
      orderBy
      ​、
      having
      ​等,使得开发者可以更加方便地构建复杂的查询条件。

1.3 特点

MyBatis-Plus的条件构造器具有以下特点:

  1. 链式调用
    :Wrapper类允许开发者以链式调用的方式构造查询条件,无需编写繁琐的SQL语句,从而提高开发效率。
  2. 安全性
    :通过使用Wrapper,可以避免直接拼接SQL片段,减少SQL注入的风险。
  3. 灵活性
    :支持多种查询操作,如等于、不等于、大于、小于等,以及逻辑组合如
    and
    ​和
    or
    ​。
  4. Lambda表达式
    :LambdaQueryWrapper和LambdaUpdateWrapper通过Lambda表达式引用实体类的属性,避免了硬编码字段名,提高了代码的可读性和可维护性。
  5. 减少代码量
    :Wrappers类作为一个静态工厂类,可以快速创建Wrapper实例,减少代码量,提高开发效率。
  6. 线程安全性
    :Wrapper实例不是线程安全的,建议每次使用时创建新的Wrapper实例,以避免多线程环境下的数据竞争和潜在错误。
  7. 支持复杂查询
    :支持嵌套查询和自定义SQL片段,通过
    nested
    ​和
    apply
    ​方法,可以构建更复杂的查询条件。
  8. 类型处理器
    :在Wrapper中可以使用TypeHandler处理特殊的数据类型,增强了对数据库类型的支持。
  9. 更新操作简化
    :使用UpdateWrapper或LambdaUpdateWrapper时,可以省略实体对象,直接在Wrapper中设置更新字段。

1.4 主要类型

MyBatis-Plus 提供了多种条件构造器,以满足不同的查询需求:

  1. QueryWrapper<T>
    :用于构建查询条件,支持链式调用,可以非常方便地添加各种查询条件。
  2. UpdateWrapper<T>
    :用于构建更新条件,支持链式调用,可以方便地添加各种更新条件。
  3. LambdaQueryWrapper<T>
    :使用 Lambda 表达式来构建查询条件,避免了字段名错误的问题,增强了代码的可读性和健壮性。
  4. LambdaUpdateWrapper<T>
    :使用 Lambda 表达式来构建更新条件,同样可以避免字段名错误的问题。
  5. AbstractWrapper<T>
    :是一个抽象类,其他 Wrapper 类继承自这个类,提供了一些基础的方法实现。

1e12f7c4-aa70-4024-bbbc-f820a4772f8c

二、基本运用

2.1
使用方法

条件构造器允许开发者以链式调用的方式构造SQL的WHERE子句,提供了极大的灵活性和便利性。例如,使用QueryWrapper可以这样构建查询条件:

QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name", "Kimi").lt("age", 30);

这将生成SQL:
SELECT * FROM user WHERE name = 'Kimi' AND age < 30
​。

2.2 示例

QueryWrapper 示例

// 创建 QueryWrapper 对象
QueryWrapper<User> queryWrapper = new QueryWrapper<>();

// 添加查询条件
queryWrapper.eq("name", "张三") // 字段等于某个值
            .gt("age", 18)      // 字段大于某个值
            .like("email", "%@gmail.com"); // 字段包含某个值

// 使用条件进行查询
List<User> users = userMapper.selectList(queryWrapper);

UpdateWrapper 示例

// 创建 UpdateWrapper 对象
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();

// 设置更新条件
updateWrapper.eq("id", 1); // 更新 id=1 的记录

// 设置要更新的数据
User user = new User();
user.setName("李四");
user.setAge(20);

// 执行更新操作
int result = userMapper.update(user, updateWrapper);

LambdaQueryWrapper 示例

// 创建 LambdaQueryWrapper 对象
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();

// 添加查询条件
lambdaQueryWrapper.eq(User::getName, "张三")
                  .gt(User::getAge, 18)
                  .like(User::getEmail, "%@gmail.com");

// 使用条件进行查询
List<User> users = userMapper.selectList(lambdaQueryWrapper);

三、Wrapper 类

3.1 简介

在 MyBatis-Plus 中,Wrapper 类是构建查询和更新条件的核心工具。

image

image

3.2 方法

MyBatis-Plus的Wrapper类提供了一系列方法来构建复杂的数据库查询条件。以下是一些常用的Wrapper类方法汇总:

  1. 基本条件方法


    • eq
      ​:等于条件,例如
      wrapper.eq("name", "zhangsan")
      ​。
    • ne
      ​:不等于条件,例如
      wrapper.ne("name", "
      zhangsan
      ")
      ​。
    • gt
      ​:大于条件,例如
      wrapper.gt("age", 18)
      ​。
    • lt
      ​:小于条件,例如
      wrapper.lt("age", 18)
      ​。
    • ge
      ​:大于等于条件,例如
      wrapper.ge("age", 18)
      ​。
    • le
      ​:小于等于条件,例如
      wrapper.le("age", 18)
      ​。
    • between
      ​:介于两个值之间,例如
      wrapper.between("age", 18, 30)
      ​。
    • notBetween
      ​:不介于两个值之间,例如
      wrapper.notBetween("age", 18, 30)
      ​。
    • like
      ​:模糊匹配,例如
      wrapper.like("name", "%zhangsan%")
      ​。
    • notLike
      ​:不模糊匹配,例如
      wrapper.notLike("name", "%zhangsan%")
      ​。
    • likeLeft
      ​:左模糊匹配,例如
      wrapper.likeLeft("name", "zhangsan%")
      ​。
    • likeRight
      ​:右模糊匹配,例如
      wrapper.likeRight("name", "%zhangsan")
      ​。
    • isNull
      ​:字段值为null,例如
      wrapper.isNull("name")
      ​。
    • isNotNull
      ​:字段值不为null,例如
      wrapper.isNotNull("name")
      ​。
    • in
      ​:字段值在指定集合中,例如
      wrapper.in("name", "zhangsan", "Tom")
      ​。
    • notIn
      ​:字段值不在指定集合中,例如
      wrapper.notIn("name", "zhangsan", "Tom")
      ​。
  2. 逻辑组合方法


    • and
      ​:添加一个AND条件,例如
      wrapper.and(wq -> wq.eq("name", "zhangsan"))
      ​。
    • or
      ​:添加一个OR条件,例如
      wrapper.or(wq -> wq.eq("name", "zhangsan"))
      ​。
  3. SQL片段方法


    • apply
      ​:添加自定义SQL片段,例如
      wrapper.apply("name = {0}", "zhangsan")
      ​。
    • last
      ​:添加自定义SQL片段到末尾,例如
      wrapper.last("order by name")
      ​。
  4. 子查询方法


    • inSql
      ​:子查询IN条件,例如
      wrapper.inSql("name", "select name from user where age > 21")
      ​。
    • notInSql
      ​:子查询NOT IN条件,例如
      wrapper.notInSql("name", "select name from user where age > 21")
      ​。
  5. 分组与排序方法


    • groupBy
      ​:分组,例如
      wrapper.groupBy("name")
      ​。
    • orderByAsc
      ​:升序排序,例如
      wrapper.orderByAsc("age")
      ​。
    • orderByDesc
      ​:降序排序,例如
      wrapper.orderByDesc("age")
      ​。
  6. 其他方法


    • exists
      ​:存在条件,例如
      wrapper.exists("select * from user where name = {0}", "zhangsan")
      ​。

    • notExists
      ​:不存在条件,例如
      wrapper.notExists("select * from user where name = {0}", "zhangsan")
      ​。

    • set
      ​:更新操作时设置字段值,例如
      updateWrapper.set("name", "zhangsan")
      ​。

    • having(String column, Object val): HAVING 过滤条件,用于聚合后的过滤,例如

      queryWrapper.select("name", "age")
                          .groupBy("age")
                          .having("count(id) > 1");
      

以上方法提供了构建查询和更新条件的灵活性和强大功能,使得MyBatis-Plus在数据库操作方面更加高效和安全。

在 chatGPT 的推动下。LLM 简直火出天际,各行各业都在蹭。听说最近 meta 开源的 llama3 模型可以轻松在普通 PC 上运行,这让我也忍不住来蹭一层。以下是使用 ollama 试玩 llama3 的一些记录。

什么是 llama

LLaMA(Large Language Model Meta AI)是Meta开发的大规模预训练语言模型,基于Transformer架构,具有强大的自然语言处理能力。它在文本生成、问答系统、机器翻译等任务中表现出色。LLaMA模型有多个规模,从几亿到上千亿参数,适用于不同的应用场景。用户可以通过开源平台如Hugging Face获取LLaMA模型,并根据需要进行微调。LLaMA的灵活性和可扩展性使其在自然语言处理领域具有广泛的应用前景。

什么是 ollama

Ollama是一款用于本地安装和管理大规模预训练语言模型的工具。它简化了模型的下载、安装和使用流程,支持多种流行的模型如GPT-4和llama。Ollama通过易于使用的命令行界面和API,帮助用户快速部署和运行自然语言处理任务。它还支持多GPU配置和模型微调,适应各种计算资源和应用需求。总之,Ollama为研究人员和开发者提供了一个高效、灵活的本地化大模型解决方案。

下载 ollama

ollama 官网提供了各种平台的安装包,那么这里选择 windows 系统的。以下是下载地址:
https://ollama.com/download

在 windows 上安装

在 windows 上安装那简直太简单了,一路 next 就行了。

安装成功后可以在命令行下执行

ollama -v


如果能成功打印出版本信息,那么说明你安装成功了。

下载模型并运行

安装好 ollama 之后我们需要把训练好的模型拉到本地,然后才能运行它。

查找 模型

ollama 提供了一个页面供用户查询可以使用的开源模型。

https://ollama.com/search?q=&p=1

可以看到主流的开源 LLM 几乎都能找到。什么 llama3 啊,phi3 啊,国产的 qwen2 啊。让我们点击 llama3 看看详情。

里面可以选模型的参数大小。这里我们选 8b 试一下。模型大小是 4.7 GB。复制右上角的命令并在命令行运行:

ollama run llama3:8b

程序会开始下载模型到本地。这里得夸一下,ollama 是不是在国内接了 CDN,这速度杠杆的,直接跑满了我的千兆网络。

对话

下载完成后命令行就会跳转到对话模型,等待你输入问题。随便先来一个吧。
Q:飞机为什么会飞?
A: balabala 一大堆,都是英文。

Q: what is SOLID principle?
A:

总结

到这,我们本地运行大模型基本上是初步成功了。简直超级无敌简单,属于有手就行。问题就是本地限制于PC的性能,回答的速度比较慢,大概一秒2-3个单词。CPU大概吃掉50%。当然如果你有 N 卡可能会好很多。内存倒是还好才吃了300多M。好了,下一次我们来试试 open-webui,把本地的模型搞的跟 chatGPT 一样。

【1】引言(完整代码在最后面)

在本文中,我们将介绍如何使用鸿蒙系统(HarmonyOS)开发一个简单的指南针应用。通过这个案例,你可以学习如何使用传感器服务、状态管理以及UI构建等基本技能。

【2】环境准备

电脑系统:windows 10

开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806

工程版本:API 12

真机:Mate 60 Pro

语言:ArkTS、ArkUI

【3】算法分析

1. 角度差计算算法

计算当前角度与目标角度之间的差值,考虑了角度的周期性(0度和360度等效)。

private calculateAngleDifference(currentAngle: number, targetAngle: number): number {
    let diff = targetAngle - currentAngle;

    if (diff > 180) {
        diff -= 360; // 顺时针旋转超过180度,调整为负值
    } else if (diff < -180) {
        diff += 360; // 逆时针旋转超过180度,调整为正值
    }

    return diff;
}

2. 累计旋转角度算法

累计计算旋转角度,确保角度在0到360度之间。以便旋转动画能正确实现

private updateRotationAngle(angleDifference: number, newAngle: number): void {
    this.cumulativeRotation += angleDifference; // 累加旋转角度
    this.rotationAngle += angleDifference; // 更新当前旋转角度
    this.currentAngle = newAngle; // 更新当前传感器角度

    this.rotationAngle = (this.rotationAngle % 360 + 360) % 360; // 保持在0到360度之间
}

3. 方向计算算法

根据传感器角度计算当前方向,匹配角度范围对应的方向名称。

private calculateDirection(angle: number): string {
    for (const range of DIRECTION_RANGES) {
        if (angle >= range.min && angle < range.max) {
            return range.name; // 返回对应的方向名称
        }
    }
    return '未知方向'; // 如果角度不在任何范围内,返回未知方向
}

【完整代码】

import { sensor } from '@kit.SensorServiceKit'; // 导入传感器服务模块
import { BusinessError } from '@kit.BasicServicesKit'; // 导入业务错误处理模块

// 定义方向范围类
class DirectionRange {
  name: string = ''; // 方向名称
  min: number = 0; // 最小角度
  max: number = 0; // 最大角度
}

// 定义各个方向的范围
const DIRECTION_RANGES: DirectionRange[] = [
  { name: '北', min: 337.5, max: 360 },
  { name: '北', min: 0, max: 22.5 },
  { name: '东北', min: 22.5, max: 67.5 },
  { name: '东', min: 67.5, max: 112.5 },
  { name: '东南', min: 112.5, max: 157.5 },
  { name: '南', min: 157.5, max: 202.5 },
  { name: '西南', min: 202.5, max: 247.5 },
  { name: '西', min: 247.5, max: 292.5 },
  { name: '西北', min: 292.5, max: 337.5 }
];

// 定义指南针组件
@Entry
@Component
struct Compass {
  @State directionMessage: string = ''; // 当前方向的名称
  @State rotationAngle: number = 0; // 当前旋转角度
  @State currentAngle: number = 0; // 当前传感器角度
  @State cumulativeRotation: number = 0; // 累计旋转角度
  private threshold: number = 1; // 设置阈值,用于过滤小的旋转变化

  // 组件即将出现时调用
  aboutToAppear(): void {
    sensor.getSensorList((error: BusinessError) => {
      if (error) {
        console.error('获取传感器列表失败', error); // 如果获取传感器列表失败,打印错误信息
        return;
      }
      this.startOrientationUpdates(); // 开始监听传感器数据
    });
  }

  // 开始监听传感器的方位数据
  private startOrientationUpdates(): void {
    sensor.on(sensor.SensorId.ORIENTATION, (orientationData) => {
      const alpha = orientationData.alpha; // 获取当前的方位角
      this.directionMessage = this.calculateDirection(alpha); // 计算当前方向
      const angleDifference = this.calculateAngleDifference(this.currentAngle, alpha); // 计算角度差

      if (Math.abs(angleDifference) > this.threshold) { // 如果角度变化超过阈值
        this.updateRotationAngle(angleDifference, alpha); // 更新旋转角度
      }
    }, { interval: 10000000 }); // 设置传感器更新间隔,单位为纳秒,10000000表示1秒
  }

  // 计算两个角度之间的差异
  private calculateAngleDifference(currentAngle: number, targetAngle: number): number {
    let diff = targetAngle - currentAngle; // 计算角度差

    if (diff > 180) {
      diff -= 360; // 顺时针旋转超过180度,调整为负值
    } else if (diff < -180) {
      diff += 360; // 逆时针旋转超过180度,调整为正值
    }

    return diff; // 返回调整后的角度差
  }

  // 更新旋转角度
  private updateRotationAngle(angleDifference: number, newAngle: number): void {
    this.cumulativeRotation += angleDifference; // 累加旋转角度
    this.rotationAngle += angleDifference; // 更新当前旋转角度
    this.currentAngle = newAngle; // 更新当前传感器角度

    // 动画更新
    animateToImmediately({}, () => {
      this.rotationAngle = this.cumulativeRotation; // 将旋转角度设置为累计旋转角度
    });

    console.log(`累计旋转角度: ${this.cumulativeRotation}`); // 打印累计旋转角度
  }

  // 根据角度计算方向
  private calculateDirection(angle: number): string {
    for (const range of DIRECTION_RANGES) {
      if (angle >= range.min && angle < range.max) {
        return range.name; // 返回对应的方向名称
      }
    }
    return '未知方向'; // 如果角度不在任何范围内,返回未知方向
  }

  // 构建用户界面
  build() {
    Column({ space: 20 }) { // 创建一个列布局,设置间距为20
      Row({ space: 5 }) { // 创建一个行布局,设置间距为5
        Text(this.directionMessage) // 显示当前方向
          .layoutWeight(1) // 设置布局权重
          .textAlign(TextAlign.End) // 文本对齐方式
          .fontColor('#dedede') // 文本颜色
          .fontSize(50); // 文本大小
        Text(`${Math.floor(this.currentAngle)}°`) // 显示当前角度
          .layoutWeight(1) // 设置布局权重
          .textAlign(TextAlign.Start) // 文本对齐方式
          .fontColor('#dedede') // 文本颜色
          .fontSize(50); // 文本大小
      }.width('100%').margin({ top: 50 }); // 设置宽度和上边距

      Stack() { // 创建一个堆叠布局
        Stack() { // 内部堆叠布局
          Circle() // 创建一个圆形
            .width(250) // 设置宽度
            .height(250) // 设置高度
            .fillOpacity(0) // 设置填充透明度
            .strokeWidth(25) // 设置边框宽度
            .stroke('#f95941') // 设置边框颜色
            .strokeDashArray([1, 5]) // 设置边框虚线样式
            .strokeLineJoin(LineJoinStyle.Round); // 设置边框连接方式
          Text('北') // 创建一个文本,显示“北”
            .height('100%') // 设置高度
            .width(40) // 设置宽度
            .align(Alignment.Top) // 设置对齐方式
            .fontColor('#ff4f3f') // 设置文本颜色
            .rotate({ angle: 0 }) // 设置旋转角度
            .padding({ top: 80 }) // 设置内边距
            .textAlign(TextAlign.Center); // 设置文本对齐方式
          Text('东') // 创建一个文本,显示“东”
            .height('100%') // 设置高度
            .width(40) // 设置宽度
            .align(Alignment.Top) // 设置对齐方式
            .fontColor('#fcfdfd') // 设置文本颜色
            .rotate({ angle: 90 }) // 设置旋转角度
            .padding({ top: 80 }) // 设置内边距
            .textAlign(TextAlign.Center); // 设置文本对齐方式
          Text('南') // 创建一个文本,显示“南”
            .height('100%') // 设置高度
            .width(40) // 设置宽度
            .align(Alignment.Top) // 设置对齐方式
            .fontColor('#fcfdfd') // 设置文本颜色
            .rotate({ angle: 180 }) // 设置旋转角度
            .padding({ top: 80 }) // 设置内边距
            .textAlign(TextAlign.Center); // 设置文本对齐方式
          Text('西') // 创建一个文本,显示“西”
            .height('100%') // 设置高度
            .width(40) // 设置宽度
            .align(Alignment.Top) // 设置对齐方式
            .fontColor('#fcfdfd') // 设置文本颜色
            .rotate({ angle: 270 }) // 设置旋转角度
            .padding({ top: 80 }) // 设置内边距
            .textAlign(TextAlign.Center); // 设置文本对齐方式
        }
        .width('100%') // 设置宽度
        .height('100%') // 设置高度
        .borderRadius('50%') // 设置圆角
        .margin({ top: 50 }) // 设置上边距
        .rotate({ angle: -this.rotationAngle }) // 设置旋转角度
        .animation({}); // 设置动画效果

        Line() // 创建一个线条
          .width(5) // 设置宽度
          .height(40) // 设置高度
          .backgroundColor('#fdfffe') // 设置背景颜色
          .borderRadius('50%') // 设置圆角
          .margin({ bottom: 200 }); // 设置下边距
      }
      .width(300) // 设置宽度
      .height(300); // 设置高度
    }
    .height('100%') // 设置高度
    .width('100%') // 设置宽度
    .backgroundColor('#18181a'); // 设置背景颜色
  }
}