From 42009d2974a5cbcdf789dfa1157dcf73b85617c4 Mon Sep 17 00:00:00 2001 From: Roy Han Date: Thu, 25 Jul 2024 17:34:11 -0700 Subject: [PATCH] just for fun --- cmd/cmd.go | 256 +++++++++++++++++++++++++++++++++++++++++++++ cmd/interactive.go | 140 ------------------------- plot.png | Bin 9420 -> 0 bytes 3 files changed, 256 insertions(+), 140 deletions(-) delete mode 100644 plot.png diff --git a/cmd/cmd.go b/cmd/cmd.go index b761d018f..4d2d1561d 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -2,6 +2,7 @@ package cmd import ( "archive/zip" + "bufio" "bytes" "context" "crypto/ed25519" @@ -16,6 +17,7 @@ import ( "net" "net/http" "os" + "os/exec" "os/signal" "path/filepath" "regexp" @@ -31,6 +33,11 @@ import ( "github.com/spf13/cobra" "golang.org/x/crypto/ssh" "golang.org/x/term" + "gonum.org/v1/gonum/mat" + "gonum.org/v1/gonum/stat" + "gonum.org/v1/plot" + "gonum.org/v1/plot/plotter" + "gonum.org/v1/plot/vg" "github.com/ollama/ollama/api" "github.com/ollama/ollama/auth" @@ -370,6 +377,90 @@ func RunHandler(cmd *cobra.Command, args []string) error { return generate(cmd, opts) } +func EmbedHandler(cmd *cobra.Command, args []string) error { + interactive := true + + opts := runOptions{ + Model: args[0], + WordWrap: os.Getenv("TERM") == "xterm-256color", + Options: map[string]interface{}{}, + } + + format, err := cmd.Flags().GetString("format") + if err != nil { + return err + } + opts.Format = format + + keepAlive, err := cmd.Flags().GetString("keepalive") + if err != nil { + return err + } + if keepAlive != "" { + d, err := time.ParseDuration(keepAlive) + if err != nil { + return err + } + opts.KeepAlive = &api.Duration{Duration: d} + } + + prompts := args[1:] + // prepend stdin to the prompt if provided + if !term.IsTerminal(int(os.Stdin.Fd())) { + in, err := io.ReadAll(os.Stdin) + if err != nil { + return err + } + + prompts = append([]string{string(in)}, prompts...) + opts.WordWrap = false + interactive = false + } + opts.Prompt = strings.Join(prompts, " ") + if len(prompts) > 0 { + interactive = false + } + + nowrap, err := cmd.Flags().GetBool("nowordwrap") + if err != nil { + return err + } + opts.WordWrap = !nowrap + + // Fill out the rest of the options based on information about the + // model. + client, err := api.ClientFromEnvironment() + if err != nil { + return err + } + + name := args[0] + info, err := func() (*api.ShowResponse, error) { + showReq := &api.ShowRequest{Name: name} + info, err := client.Show(cmd.Context(), showReq) + var se api.StatusError + if errors.As(err, &se) && se.StatusCode == http.StatusNotFound { + if err := PullHandler(cmd, []string{name}); err != nil { + return nil, err + } + return client.Show(cmd.Context(), &api.ShowRequest{Name: name}) + } + return info, err + }() + if err != nil { + return err + } + + opts.MultiModal = slices.Contains(info.Details.Families, "clip") + opts.ParentModel = info.Details.ParentModel + opts.Messages = append(opts.Messages, info.Messages...) + + if interactive { + return generateInteractive(cmd, opts) + } + return embed(cmd, opts) +} + func errFromUnknownKey(unknownKeyErr error) error { // find SSH public key in the error message sshKeyPattern := `ssh-\w+ [^\s"]+` @@ -979,6 +1070,154 @@ func chat(cmd *cobra.Command, opts runOptions) (*api.Message, error) { return &api.Message{Role: role, Content: fullResponse.String()}, nil } +func embed(cmd *cobra.Command, opts runOptions) error { + line := opts.Prompt + client, err := api.ClientFromEnvironment() + if err != nil { + fmt.Println("error: couldn't connect to ollama server") + return err + } + + inputs := strings.Split(line, "\n\n") + + req := &api.EmbedRequest{ + Model: opts.Model, + Input: inputs, + } + + resp, err := client.Embed(cmd.Context(), req) + if err != nil { + fmt.Println("error: couldn't get embeddings") + return err + } + + embeddings := resp.Embeddings + + r, c := len(embeddings), len(embeddings[0]) + data := make([]float64, r*c) + for i := range r { + for j := range c { + data[i*c+j] = float64(embeddings[i][j]) + } + } + + X := mat.NewDense(r, c, data) + + // Initialize PCA + var pca stat.PC + + // Perform PCA + if !pca.PrincipalComponents(X, nil) { + return fmt.Errorf("PCA failed") + } + + // Extract principal component vectors + var vectors mat.Dense + pca.VectorsTo(&vectors) + + // // Extract variances of the principal components + // var variances []float64 + // variances = pca.VarsTo(variances) + + W := vectors.Slice(0, c, 0, 2).(*mat.Dense) + + // Perform PCA reduction + var reducedData mat.Dense + reducedData.Mul(X, W) + + for i, s := range inputs { + row := reducedData.RowView(i) + fmt.Print(i+1, ". ", s, "\n") + fmt.Printf("[%v, %v]\n\n", row.AtVec(0), row.AtVec(1)) + } + + points := make(plotter.XYs, reducedData.RawMatrix().Rows) + for i := range len(points) { + row := reducedData.RowView(i) + points[i].X = row.AtVec(0) + points[i].Y = row.AtVec(1) + } + + // Create a new plot + p := plot.New() + + // Set plot title and axis labels + p.Title.Text = "Embedding Map" + + // Create a scatter plot of the points + s, err := plotter.NewScatter(points) + if err != nil { + panic(err) + } + p.Add(s) + + /// Create labels plotter and add it to the plot + + labels := make([]string, reducedData.RawMatrix().Rows) + for i := range len(labels) { + labels[i] = fmt.Sprintf("%d", i+1) + } + + // plotter := plotter + + l, err := plotter.NewLabels(plotter.XYLabels{XYs: points, Labels: labels}) + if err != nil { + panic(err) + } + p.Add(l) + + // Make the grid square + p.X.Min = -1 + p.X.Max = 1 + p.Y.Min = -1 + p.Y.Max = 1 + + // Set the aspect ratio to be 1:1 + p.X.Tick.Marker = plot.ConstantTicks([]plot.Tick{ + {Value: -1, Label: "-1"}, + {Value: -0.5, Label: "-0.5"}, + {Value: 0, Label: "0"}, + {Value: 0.5, Label: "0.5"}, + {Value: 1, Label: "1"}, + }) + p.Y.Tick.Marker = plot.ConstantTicks([]plot.Tick{ + {Value: -1, Label: "-1"}, + {Value: -0.5, Label: "-0.5"}, + {Value: 0, Label: "0"}, + {Value: 0.5, Label: "0.5"}, + {Value: 1, Label: "1"}, + }) + + // Save the plot to a svg file + if err := p.Save(6*vg.Inch, 6*vg.Inch, "plot.svg"); err != nil { + panic(err) + } + + // open the plot + open := exec.Command("open", "plot.svg") + err = open.Run() + if err != nil { + fmt.Println("error: couldn't open plot") + return err + } + + // Wait for Enter key press + fmt.Print("Press 'Enter' to continue") + reader := bufio.NewReader(os.Stdin) + _, _ = reader.ReadString('\n') + + // close and delete the plot (defer this) + defer func() { + delete := exec.Command("rm", "plot.svg") + err = delete.Run() + if err != nil { + fmt.Println("error: couldn't delete plot") + } + }() + + return nil +} + func generate(cmd *cobra.Command, opts runOptions) error { client, err := api.ClientFromEnvironment() if err != nil { @@ -1247,11 +1486,26 @@ func NewCLI() *cobra.Command { RunE: RunHandler, } + embedCmd := &cobra.Command{ + Use: "embed MODEL [PROMPT]", + Short: "Embed a model", + Args: cobra.MinimumNArgs(1), + PreRunE: checkServerHeartbeat, + RunE: EmbedHandler, + } + runCmd.Flags().String("keepalive", "", "Duration to keep a model loaded (e.g. 5m)") runCmd.Flags().Bool("verbose", false, "Show timings for response") runCmd.Flags().Bool("insecure", false, "Use an insecure registry") runCmd.Flags().Bool("nowordwrap", false, "Don't wrap words to the next line automatically") runCmd.Flags().String("format", "", "Response format (e.g. json)") + + embedCmd.Flags().String("keepalive", "", "Duration to keep a model loaded (e.g. 5m)") + embedCmd.Flags().Bool("verbose", false, "Show timings for response") + embedCmd.Flags().Bool("insecure", false, "Use an insecure registry") + embedCmd.Flags().Bool("nowordwrap", false, "Don't wrap words to the next line automatically") + embedCmd.Flags().String("format", "", "Response format (e.g. json)") + serveCmd := &cobra.Command{ Use: "serve", Aliases: []string{"start"}, @@ -1326,6 +1580,7 @@ func NewCLI() *cobra.Command { copyCmd, deleteCmd, serveCmd, + embedCmd, } { switch cmd { case runCmd: @@ -1361,6 +1616,7 @@ func NewCLI() *cobra.Command { psCmd, copyCmd, deleteCmd, + embedCmd, ) return rootCmd diff --git a/cmd/interactive.go b/cmd/interactive.go index 71d49e021..33f7fa5b1 100644 --- a/cmd/interactive.go +++ b/cmd/interactive.go @@ -1,7 +1,6 @@ package cmd import ( - "encoding/json" "errors" "fmt" "io" @@ -14,11 +13,6 @@ import ( "strings" "github.com/spf13/cobra" - "gonum.org/v1/gonum/mat" - "gonum.org/v1/gonum/stat" - "gonum.org/v1/plot" - "gonum.org/v1/plot/plotter" - "gonum.org/v1/plot/vg" "github.com/ollama/ollama/api" "github.com/ollama/ollama/envconfig" @@ -453,140 +447,6 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error { } case strings.HasPrefix(line, "/exit"), strings.HasPrefix(line, "/bye"): return nil - case strings.HasPrefix(line, "/embed"): - line = strings.TrimPrefix(line, "/embed") - client, err := api.ClientFromEnvironment() - if err != nil { - fmt.Println("error: couldn't connect to ollama server") - return err - } - - var strArray []string - fmt.Printf("line is %s\n", line) - err = json.Unmarshal([]byte(line), &strArray) - if err != nil { - fmt.Println("error: couldn't parse input") - return err - } - - for i, s := range strArray { - fmt.Printf("strArray[%d] is %s\n", i, s) - } - - req := &api.EmbedRequest{ - Model: opts.Model, - Input: strArray, - } - - resp, err := client.Embed(cmd.Context(), req) - if err != nil { - fmt.Println("error: couldn't get embeddings") - return err - } - - embeddings := resp.Embeddings - - r, c := len(embeddings), len(embeddings[0]) - data := make([]float64, r*c) - for i := 0; i < r; i++ { - for j := 0; j < c; j++ { - data[i*c+j] = float64(embeddings[i][j]) - } - } - - X := mat.NewDense(r, c, data) - - // Initialize PCA - var pca stat.PC - - // Perform PCA - if !pca.PrincipalComponents(X, nil) { - return fmt.Errorf("PCA failed") - } - - // Extract principal component vectors - var vectors mat.Dense - pca.VectorsTo(&vectors) - - // // Extract variances of the principal components - // var variances []float64 - // variances = pca.VarsTo(variances) - - W := vectors.Slice(0, c, 0, 2).(*mat.Dense) - - // Perform PCA reduction - var reducedData mat.Dense - reducedData.Mul(X, W) - - // Print the projected 2D points - fmt.Println("Reduced embeddings to 2D:") - for i := 0; i < reducedData.RawMatrix().Rows; i++ { - row := reducedData.RowView(i) - fmt.Printf("[%v, %v]\n", row.AtVec(0), row.AtVec(1)) - } - - points := make(plotter.XYs, reducedData.RawMatrix().Rows) - for i := 0; i < reducedData.RawMatrix().Rows; i++ { - row := reducedData.RowView(i) - points[i].X = row.AtVec(0) - points[i].Y = row.AtVec(1) - } - - // Create a new plot - p := plot.New() - - // Set plot title and axis labels - p.Title.Text = "2D Data Plot" - p.X.Label.Text = "X" - p.Y.Label.Text = "Y" - - // Create a scatter plot of the points - s, err := plotter.NewScatter(points) - if err != nil { - panic(err) - } - p.Add(s) - - /// Create labels plotter and add it to the plot - - labels := make([]string, reducedData.RawMatrix().Rows) - for i := 0; i < reducedData.RawMatrix().Rows; i++ { - labels[i] = fmt.Sprintf("%d", i+1) - } - - l, err := plotter.NewLabels(plotter.XYLabels{XYs: points, Labels: labels}) - if err != nil { - panic(err) - } - p.Add(l) - - // Make the grid square - p.X.Min = -1 - p.X.Max = 1 - p.Y.Min = -1 - p.Y.Max = 1 - - // Set the aspect ratio to be 1:1 - p.X.Tick.Marker = plot.ConstantTicks([]plot.Tick{ - {Value: -1, Label: "-1"}, - {Value: -0.5, Label: "-0.5"}, - {Value: 0, Label: "0"}, - {Value: 0.5, Label: "0.5"}, - {Value: 1, Label: "1"}, - }) - p.Y.Tick.Marker = plot.ConstantTicks([]plot.Tick{ - {Value: -1, Label: "-1"}, - {Value: -0.5, Label: "-0.5"}, - {Value: 0, Label: "0"}, - {Value: 0.5, Label: "0.5"}, - {Value: 1, Label: "1"}, - }) - - // Save the plot to a PNG file - if err := p.Save(6*vg.Inch, 6*vg.Inch, "plot.png"); err != nil { - panic(err) - } - case strings.HasPrefix(line, "/"): args := strings.Fields(line) isFile := false diff --git a/plot.png b/plot.png deleted file mode 100644 index 1a35cd0d5535d4e2590c69b268a7b0c8949eb0d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9420 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 E8$?c!0{{R3